From ade19de410e0a789a921ec1666b278b2a64176d6 Mon Sep 17 00:00:00 2001 From: Alon Levy Date: Tue, 16 Dec 2014 17:37:42 +0200 Subject: moving files around after repository merger --- src/ScrollTo.js | 17 - src/app.js | 31 - src/buttons.js | 82 -- src/client-tests/anotherScript.js | 2 + src/client-tests/base.js | 87 ++ src/client-tests/commit_run_tests_output_update | 3 + src/client-tests/cri.json | 5 + src/client-tests/output/test_analyzer.js.stderr | 0 src/client-tests/output/test_analyzer.js.stdout | 82 ++ src/client-tests/output/test_app.js.stderr | 0 src/client-tests/output/test_app.js.stdout | 1 + src/client-tests/output/test_globals.js.stderr | 0 src/client-tests/output/test_globals.js.stdout | 11 + src/client-tests/output/test_require_js.js.stderr | 0 src/client-tests/output/test_require_js.js.stdout | 17 + .../output/test_script_jsdom.js.stderr | 0 .../output/test_script_jsdom.js.stdout | 1 + src/client-tests/output/test_util.js.stderr | 0 src/client-tests/output/test_util.js.stdout | 0 src/client-tests/run_tests.js | 31 + src/client-tests/test2.js | 4 + src/client-tests/test_analyzer.js | 49 ++ src/client-tests/test_app.js | 32 + src/client-tests/test_globals.js | 30 + src/client-tests/test_require_js.js | 25 + src/client-tests/test_script_jsdom.js | 9 + src/client-tests/test_util.js | 44 ++ src/client-tests/util/RhiziHTTPServer.py | 78 ++ src/client-tests/weizmann.json | 1 + src/client/ScrollTo.js | 17 + src/client/app.js | 31 + src/client/buttons.js | 82 ++ src/client/consts.js | 25 + src/client/drag_n_drop.js | 31 + src/client/history.js | 149 ++++ src/client/main.js | 94 +++ src/client/model/core.js | 187 +++++ src/client/model/diff.js | 187 +++++ src/client/model/graph.js | 873 +++++++++++++++++++++ src/client/model/util.js | 134 ++++ src/client/robot.js | 70 ++ src/client/rz_api_backend.js | 222 ++++++ src/client/rz_api_mesh.js | 22 + src/client/rz_bus.js | 15 + src/client/rz_config.js | 9 + src/client/rz_core.js | 756 ++++++++++++++++++ src/client/rz_observer.js | 114 +++ src/client/textanalysis.js | 501 ++++++++++++ src/client/textanalysis.ui.js | 195 +++++ src/client/util.js | 72 ++ src/client/view/completer.js | 229 ++++++ src/client/view/edge_info.js | 36 + src/client/view/helpers.js | 87 ++ src/client/view/internal.js | 14 + src/client/view/node_info.js | 117 +++ src/client/view/selection.js | 102 +++ src/client/view/tab.js | 57 ++ src/client/view/timeline.js | 180 +++++ src/client/view/view.js | 12 + src/consts.js | 25 - src/drag_n_drop.js | 31 - src/history.js | 149 ---- src/main.js | 94 --- src/model/core.js | 187 ----- src/model/diff.js | 187 ----- src/model/graph.js | 873 --------------------- src/model/util.js | 134 ---- src/robot.js | 70 -- src/rz_api_backend.js | 222 ------ src/rz_api_mesh.js | 22 - src/rz_bus.js | 15 - src/rz_config.js | 9 - src/rz_core.js | 756 ------------------ src/rz_observer.js | 114 --- src/server-tests/neo4j_test_util.py | 54 ++ src/server-tests/test_db_controller.py | 327 ++++++++ src/server-tests/test_rhizi_api.py | 84 ++ src/server/crypt_util.py | 46 ++ src/server/db_controller.py | 495 ++++++++++++ src/server/db_driver.py | 91 +++ src/server/model/__init__.py | 0 src/server/model/graph.py | 105 +++ src/server/model/model.py | 37 + src/server/neo4j_util.py | 260 ++++++ src/server/rhizi_api.py | 256 ++++++ src/server/rhizi_server.py | 263 +++++++ src/server/rhizi_server_fcgi.py | 25 + src/server/util.py | 22 + src/textanalysis.js | 501 ------------ src/textanalysis.ui.js | 195 ----- src/util.js | 72 -- src/view/completer.js | 229 ------ src/view/edge_info.js | 36 - src/view/helpers.js | 87 -- src/view/internal.js | 14 - src/view/node_info.js | 117 --- src/view/selection.js | 102 --- src/view/tab.js | 57 -- src/view/timeline.js | 180 ----- src/view/view.js | 12 - 100 files changed, 7197 insertions(+), 4620 deletions(-) delete mode 100644 src/ScrollTo.js delete mode 100644 src/app.js delete mode 100644 src/buttons.js create mode 100644 src/client-tests/anotherScript.js create mode 100644 src/client-tests/base.js create mode 100755 src/client-tests/commit_run_tests_output_update create mode 100644 src/client-tests/cri.json create mode 100644 src/client-tests/output/test_analyzer.js.stderr create mode 100644 src/client-tests/output/test_analyzer.js.stdout create mode 100644 src/client-tests/output/test_app.js.stderr create mode 100644 src/client-tests/output/test_app.js.stdout create mode 100644 src/client-tests/output/test_globals.js.stderr create mode 100644 src/client-tests/output/test_globals.js.stdout create mode 100644 src/client-tests/output/test_require_js.js.stderr create mode 100644 src/client-tests/output/test_require_js.js.stdout create mode 100644 src/client-tests/output/test_script_jsdom.js.stderr create mode 100644 src/client-tests/output/test_script_jsdom.js.stdout create mode 100644 src/client-tests/output/test_util.js.stderr create mode 100644 src/client-tests/output/test_util.js.stdout create mode 100644 src/client-tests/run_tests.js create mode 100644 src/client-tests/test2.js create mode 100644 src/client-tests/test_analyzer.js create mode 100644 src/client-tests/test_app.js create mode 100644 src/client-tests/test_globals.js create mode 100644 src/client-tests/test_require_js.js create mode 100644 src/client-tests/test_script_jsdom.js create mode 100644 src/client-tests/test_util.js create mode 100644 src/client-tests/util/RhiziHTTPServer.py create mode 100644 src/client-tests/weizmann.json create mode 100644 src/client/ScrollTo.js create mode 100644 src/client/app.js create mode 100644 src/client/buttons.js create mode 100644 src/client/consts.js create mode 100644 src/client/drag_n_drop.js create mode 100644 src/client/history.js create mode 100644 src/client/main.js create mode 100644 src/client/model/core.js create mode 100644 src/client/model/diff.js create mode 100644 src/client/model/graph.js create mode 100644 src/client/model/util.js create mode 100644 src/client/robot.js create mode 100644 src/client/rz_api_backend.js create mode 100644 src/client/rz_api_mesh.js create mode 100644 src/client/rz_bus.js create mode 100644 src/client/rz_config.js create mode 100644 src/client/rz_core.js create mode 100644 src/client/rz_observer.js create mode 100644 src/client/textanalysis.js create mode 100644 src/client/textanalysis.ui.js create mode 100644 src/client/util.js create mode 100644 src/client/view/completer.js create mode 100644 src/client/view/edge_info.js create mode 100644 src/client/view/helpers.js create mode 100644 src/client/view/internal.js create mode 100644 src/client/view/node_info.js create mode 100644 src/client/view/selection.js create mode 100644 src/client/view/tab.js create mode 100644 src/client/view/timeline.js create mode 100644 src/client/view/view.js delete mode 100644 src/consts.js delete mode 100644 src/drag_n_drop.js delete mode 100644 src/history.js delete mode 100644 src/main.js delete mode 100644 src/model/core.js delete mode 100644 src/model/diff.js delete mode 100644 src/model/graph.js delete mode 100644 src/model/util.js delete mode 100644 src/robot.js delete mode 100644 src/rz_api_backend.js delete mode 100644 src/rz_api_mesh.js delete mode 100644 src/rz_bus.js delete mode 100644 src/rz_config.js delete mode 100644 src/rz_core.js delete mode 100644 src/rz_observer.js create mode 100644 src/server-tests/neo4j_test_util.py create mode 100644 src/server-tests/test_db_controller.py create mode 100644 src/server-tests/test_rhizi_api.py create mode 100644 src/server/crypt_util.py create mode 100644 src/server/db_controller.py create mode 100644 src/server/db_driver.py create mode 100644 src/server/model/__init__.py create mode 100644 src/server/model/graph.py create mode 100644 src/server/model/model.py create mode 100644 src/server/neo4j_util.py create mode 100644 src/server/rhizi_api.py create mode 100644 src/server/rhizi_server.py create mode 100755 src/server/rhizi_server_fcgi.py create mode 100644 src/server/util.py delete mode 100644 src/textanalysis.js delete mode 100644 src/textanalysis.ui.js delete mode 100644 src/util.js delete mode 100644 src/view/completer.js delete mode 100644 src/view/edge_info.js delete mode 100644 src/view/helpers.js delete mode 100644 src/view/internal.js delete mode 100644 src/view/node_info.js delete mode 100644 src/view/selection.js delete mode 100644 src/view/tab.js delete mode 100644 src/view/timeline.js delete mode 100644 src/view/view.js (limited to 'src') diff --git a/src/ScrollTo.js b/src/ScrollTo.js deleted file mode 100644 index a015f08c..00000000 --- a/src/ScrollTo.js +++ /dev/null @@ -1,17 +0,0 @@ -$.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 deleted file mode 100644 index 39acdde7..00000000 --- a/src/app.js +++ /dev/null @@ -1,31 +0,0 @@ -(function() { - var lib_path = '../lib/'; - var config = { - paths: { - jquery: lib_path + 'jquery', - 'jquery-ui': lib_path + 'jquery-ui', - 'd3': lib_path + 'd3/d3', - FileSaver: lib_path + 'FileSaver', - caret: lib_path + 'caret', - autocomplete: lib_path + 'autocomplete', - Bacon: lib_path + 'Bacon', - } - } - - config.urlArgs = (typeof local_config != 'undefined') && local_config.urlArgs; - -if (window.is_node) { - // Testing path only - console.log('app: running under node'); - config.baseUrl = '../src/'; - window.rhizi_require_config = config; -} else { - // Main app path - require.config(config); - - requirejs(['main'], function(main) { - console.log('starting rhizi logic'); - main.main(); - }); -} -}()); diff --git a/src/buttons.js b/src/buttons.js deleted file mode 100644 index 860c8573..00000000 --- a/src/buttons.js +++ /dev/null @@ -1,82 +0,0 @@ -"use strict" - -define(['jquery', 'FileSaver', 'rz_core'], function ($, saveAs, rz_core) { -$('.tutorial').click(function(){}); - -var key="#47989379"; - - -$('.save a').click(function(){ - var json = rz_core.graph.save_to_json(); - console.log('saving to local storage ' + json.length + ' bytes'); - localStorage.setItem(key, json); -}); - -$('.export').click(function() { - var json = rz_core.graph.save_to_json(); - var filename = 'graph.json'; - var blob = new Blob([json], {type: 'application/json'}); - console.log('saving ' + json.length + ' bytes to ' + filename); - saveAs(blob, filename); -}); - -$('.url-copy a').click(function() { - var json = rz_core.graph.save_to_json(); - // TODO use jquery BBQ $.param({json: json}); - var encoded = document.location.origin + '/?json=' + encodeURIComponent(json); - window.prompt('Copy to clipboard: Ctrl-C, Enter (or Cmd-C for Mac)', encoded); -}); - -var really_load = function() { - if (!rz_core.graph.empty()) { - return confirm('All unsaved changes will be deleted, are you sure?'); - } - return true; -} - -$('.file-import').on('change', function(event) { - var file = event.target.files[0]; - var reader; - - if (!really_load()) { - return; - } - if (file === undefined) { - return; - } - console.log(file); - reader = new FileReader(); - reader.onload = (function(theFile) { - return function(e) { - var result = e.target.result; - if (e.target.readyState === FileReader.DONE) { - console.log('done reading ' + theFile.name); - console.log('got #' + result.length + ' bytes in ' + typeof(result)); - rz_core.load_from_json(result); - } - } - })(file); - reader.readAsText(file, "text/javascript"); -}); - -$('.local-storage-load a').click(function(){ - if (!really_load()) { - return; - } - var json_blob = localStorage.getItem(key) - rz_core.load_from_json(json_blob); -}); - -var logout_button = $('#logout-button'); -logout_button.click(function() { - $.ajax({ type: "POST", url: '/logout'}); // server should redirect back to /login -}); - -$('a.save-history').click(function() { - if (rz_core.graph.history === undefined) { - throw "History is undefined"; - } - rz_core.graph.history.save_to_file(); -}); -return {'buttons': 'nothing here'}; -}); // define diff --git a/src/client-tests/anotherScript.js b/src/client-tests/anotherScript.js new file mode 100644 index 00000000..93da9fbf --- /dev/null +++ b/src/client-tests/anotherScript.js @@ -0,0 +1,2 @@ +debugger +console.log(window.__myObject); diff --git a/src/client-tests/base.js b/src/client-tests/base.js new file mode 100644 index 00000000..320edccf --- /dev/null +++ b/src/client-tests/base.js @@ -0,0 +1,87 @@ +var fs = require('fs'), + jsdom = require('jsdom'), + addScript = require('./test_util').addScript; + +function dump_nodes(window) { + var nodes = window.force.nodes(); + for(var i = 0 ; i < nodes.length; ++i) { + console.log('nodes[' + nodes[i].id + '/' + nodes[i].type + '].[xy] = (' + nodes[i].x + ',' + nodes[i].y + ')'); + } +} + +function dump_graphviz(force) { + var i; + var nodes = force.nodes(); + var links = force.links(); + var q = function(s) { + if (s.search(' ') == -1) { + return s; + } + return '"' + s + '"'; + }; + + console.log('digraph {'); + for (i = 0 ; i < nodes.length; ++i) { + if (nodes[i].type == 'bubble') { + console.log(' BUBBLE;'); + } else { + console.log(' ' + q(nodes[i].name) + ';'); + } + } + for (i = 0 ; i < links.length; ++i) { + var link = links[i]; + console.log(' ' + q(link.__src.name) + ' -> ' + q(link.__dst.name) + ' [label=' + link.name + '];'); + } + console.log('}'); +} + +function run_tests(settings) { + var document = jsdom.jsdom(''); + var window = document.parentWindow; + + window.console.log = console.log; // slightly evil + window.process = process; // more evil + window.is_node = true; + if (settings.created) { + settings.created(undefined, window); + } + debugger + addScript(window, '../src/external/require.js') + .load_next('../src/app.js') + .done(function () { + console.log('test harness: starting script loading with requirejs'); + window.require.config(window.rhizi_require_config); + window.requirejs(['main'], function(main) { + console.log('test harness: main loaded'); + main.main(); + if (settings.done) { + settings.done(undefined, window); + } + process.quit(); + }); + }); + //window.setInterval(function() { console.log('.'); return true; }, 100); +} + +function reset(window) { + window.require('rz_core').graph.clear(); +} + +function nodes(window) { + return window.require('rz_core').force.nodes().map(function (n) { + return n.name; + }); +} + +function links(window) { + return window.require('rz_core').force.links().map(function (l) { + debugger; + return [l.__src.name, l.__dst.name, l.name]; + }); +} + +exports.run_tests = run_tests; +exports.dump_graphviz = dump_graphviz; +exports.reset = reset; +exports.links = links; +exports.nodes = nodes; diff --git a/src/client-tests/commit_run_tests_output_update b/src/client-tests/commit_run_tests_output_update new file mode 100755 index 00000000..5912fb8d --- /dev/null +++ b/src/client-tests/commit_run_tests_output_update @@ -0,0 +1,3 @@ +#!/bin/bash +git add output +git commit -m "update run_tests.js output" diff --git a/src/client-tests/cri.json b/src/client-tests/cri.json new file mode 100644 index 00000000..c53c7a3d --- /dev/null +++ b/src/client-tests/cri.json @@ -0,0 +1,5 @@ +{ + "nodes":[{"name":"Jean- Christophe Thalabard","type":"person","state":"perm"},{"name":"Owen Cornec","type":"person","state":"perm"},{"name":"Vincent ALexandrine","type":"person","state":"perm"}, {"name":"Maéva Vignes","type":"person","state":"perm"}, {"name":"Abdel El Abed","type":"person","state":"perm"}, {"name":"Valérie Taly","type":"person","state":"perm"}, {"name":"Sébastien Dutreuil","type":"person","state":"perm"}, {"name":"Antoine BERGEL","type":"person","state":"perm"}, {"name":"Antoine Angot","type":"person","state":"perm"}, {"name":"Jérôme Feret","type":"person","state":"perm"}, {"name":"Charlène Gayrard","type":"person","state":"perm"}, {"name":"Hugo Jimenez Perez","type":"person","state":"perm"}, {"name":"Caterina Urban","type":"person","state":"perm"}, {"name":"Gaëlle Chevalon","type":"person","state":"perm"}, {"name":"Ian Marcus","type":"person","state":"perm"}, {"name":"Antoine TALY","type":"person","state":"perm"}, {"name":"Chantal LOTTON","type":"person","state":"perm"}, {"name":"Ana-Maria Lennon-Duménil","type":"person","state":"perm"}, {"name":"Frédérique Carlier-Grynkorn","type":"person","state":"perm"}, {"name":"Pascal Martin","type":"person","state":"perm"}, {"name":"Tamara Milosevic","type":"person","state":"perm"}, {"name":"Nicolas Carpi","type":"person","state":"perm"}, {"name":"Stéphane Daoudy","type":"person","state":"perm"}, {"name":"Danijela Matic Vignjevic","type":"person","state":"perm"}, {"name":"Eugenio Cinquemani","type":"person","state":"perm"}, {"name":"Vincent DAHIREL","type":"person","state":"perm"}, {"name":"Martin Lenz","type":"person","state":"perm"}, {"name":"Maïlys Chassagne","type":"person","state":"perm"}, {"name":"Anne Schmidt","type":"person","state":"perm"}, {"name":"Sophie Sacquin Mora","type":"person","state":"perm"}, {"name":"Richard-Emmanuel Eastes","type":"person","state":"perm"}, {"name":"Michel Morange","type":"person","state":"perm"}, {"name":"Ewa Zlotek-Zlotkiewicz","type":"person","state":"perm"}, {"name":"A.m.o.d.s.e.n C.h.o.t.i.a","type":"person","state":"perm"}, {"name":"Alexandre Vaugoux","type":"person","state":"perm"}, {"name":"Annemiek JM Cornelissen","type":"person","state":"perm"}, {"name":"Clément Nizak","type":"person","state":"perm"}, {"name":"Antoine Frenoy","type":"person","state":"perm"}, {"name":"Ariel B. Lindner","type":"person","state":"perm"}, {"name":"Benjamin Brogniart","type":"person","state":"perm"}, {"name":"Christophe Zimmer","type":"person","state":"perm"}, {"name":"Claire Ribrault","type":"person","state":"perm"}, {"name":"David Tareste","type":"person","state":"perm"}, {"name":"Denis Lafeuille","type":"person","state":"perm"}, {"name":"Dor Garbash","type":"person","state":"perm"}, {"name":"Dusan MISEVIC","type":"person","state":"perm"}, {"name":"Edda Nitschke","type":"person","state":"perm"}, {"name":"François Taddei","type":"person","state":"perm"}, {"name":"Gregory Batt","type":"person","state":"perm"}, {"name":"Jean Luc Lebrun","type":"person","state":"perm"}, {"name":"Jesse Himmelstein","type":"person","state":"perm"}, {"name":"Kevin Lhoste","type":"person","state":"perm"}, {"name":"Laura Ciriani","type":"person","state":"perm"}, {"name":"Livio Riboli-Sasco","type":"person","state":"perm"}, {"name":"Nathalie Sussfeld","type":"person","state":"perm"}, {"name":"Jake Edwin Wintermute","type":"person","state":"perm"}, {"name":"Marlyne Nogbou","type":"person","state":"perm"}, {"name":"Matthieu Piel","type":"person","state":"perm"}, {"name":"Pascal Hersen","type":"person","state":"perm"}, {"name":"Pierre-Yves Bourguignon","type":"person","state":"perm"}, {"name":"Timo Betz","type":"person","state":"perm"}, {"name":"Raphaël Goujet","type":"person","state":"perm"}, {"name":"Stéphane Debove","type":"person","state":"perm"}, {"name":"Vincent Danos","type":"person","state":"perm"}, {"name":"Tam Kien Duong","type":"person","state":"perm"}], + "links":[{"__src":"Jean- Christophe Thalabard","__dst":"Maéva Vignes","name":"works with"}], + "projects":[{"name":"Rhizi","type":"project","state":"perm"},{"name":"RedWire","type":"project","state":"perm"},{"name":"IdStorm","type":"project","state":"perm"},{"name":"Savanturiers","type":"project","state":"perm"},{"name":"Hero.coli","type":"project","state":"perm"}] +} diff --git a/src/client-tests/output/test_analyzer.js.stderr b/src/client-tests/output/test_analyzer.js.stderr new file mode 100644 index 00000000..e69de29b diff --git a/src/client-tests/output/test_analyzer.js.stdout b/src/client-tests/output/test_analyzer.js.stdout new file mode 100644 index 00000000..7e15edcb --- /dev/null +++ b/src/client-tests/output/test_analyzer.js.stdout @@ -0,0 +1,82 @@ +addScript: loaded ../src/external/require.js +app: running under node +addScript: loaded ../src/app.js +test harness: starting script loading with requirejs +test harness: main loaded +Rhizi main started +rhizi: init drag-n-drop +analyzing #a hello there #b +not same size: new/old 2 / 0; 1 / 0 +bug: temp node creation on finalize +expected: a,b +got: a|b +expected: a,b,hello there +got: a,b,hello there +digraph { + a; + b; + a -> b [label=hello there]; +} +analyzing #c and #d like #e +not same size: new/old 3 / 0; 3 / 0 +bug: temp node creation on finalize +expected: c,d,e +got: c|d|e +expected: c,e,like,d,e,like +got: c,d,and,d,e,like,c,e,like +digraph { + c; + d; + e; + c -> d [label=and]; + d -> e [label=like]; + c -> e [label=like]; +} +analyzing #f and #g and #h are cool +not same size: new/old 5 / 0; 3 / 0 +bug: temp node creation on finalize +bug: temp node creation on finalize +expected: f,g,h,f and g and h are cool +got: f|g|h|f and g and h are cool +expected: f,f and g and h are cool,,g,f and g and h are cool,,h,f and g and h are cool, +got: f,f and g and h are cool,,g,f and g and h are cool,,h,f and g and h are cool, +digraph { + f; + g; + h; + "f and g and h are cool"; + f -> "f and g and h are cool" [label=]; + g -> "f and g and h are cool" [label=]; + h -> "f and g and h are cool" [label=]; +} +analyzing #i likes #j and #k +not same size: new/old 3 / 0; 3 / 0 +bug: temp node creation on finalize +expected: i,j,k +got: i|j|k +expected: i,j,likes +got: i,j,likes,j,k,and,i,k,likes +digraph { + i; + j; + k; + i -> j [label=likes]; + j -> k [label=and]; + i -> k [label=likes]; +} +analyzing #q likes #r but doesn't like #l +not same size: new/old 4 / 0; 3 / 0 +bug: temp node creation on finalize +expected: q,r,l +got: q|r|l|q likes r but doesn't like l +expected: +got: q,q likes r but doesn't like l,,r,q likes r but doesn't like l,,l,q likes r but doesn't like l, +digraph { + q; + r; + l; + "q likes r but doesn't like l"; + q -> "q likes r but doesn't like l" [label=]; + r -> "q likes r but doesn't like l" [label=]; + l -> "q likes r but doesn't like l" [label=]; +} diff --git a/src/client-tests/output/test_app.js.stderr b/src/client-tests/output/test_app.js.stderr new file mode 100644 index 00000000..e69de29b diff --git a/src/client-tests/output/test_app.js.stdout b/src/client-tests/output/test_app.js.stdout new file mode 100644 index 00000000..2d2c7940 --- /dev/null +++ b/src/client-tests/output/test_app.js.stdout @@ -0,0 +1 @@ +test_app: running under node diff --git a/src/client-tests/output/test_globals.js.stderr b/src/client-tests/output/test_globals.js.stderr new file mode 100644 index 00000000..e69de29b diff --git a/src/client-tests/output/test_globals.js.stdout b/src/client-tests/output/test_globals.js.stdout new file mode 100644 index 00000000..9aa7f822 --- /dev/null +++ b/src/client-tests/output/test_globals.js.stdout @@ -0,0 +1,11 @@ +before fields count: 187 +addScript: loaded ../src/external/require.js +app: running under node +addScript: loaded ../src/app.js +test harness: starting script loading with requirejs +test harness: main loaded +Rhizi main started +rhizi: init drag-n-drop +after fields count: 195 +new fields count: 8 +requirejs,require,define,rhizi_require_config,$,jQuery,d3,saveAs diff --git a/src/client-tests/output/test_require_js.js.stderr b/src/client-tests/output/test_require_js.js.stderr new file mode 100644 index 00000000..e69de29b diff --git a/src/client-tests/output/test_require_js.js.stdout b/src/client-tests/output/test_require_js.js.stdout new file mode 100644 index 00000000..82ea089e --- /dev/null +++ b/src/client-tests/output/test_require_js.js.stdout @@ -0,0 +1,17 @@ +addScript: loaded ../src/external/require.js +test_app: running under node +addScript: loaded test_app.js +callback after test_app.js loading +{ paths: + { jquery: 'external/jquery', + 'jquery-ui': 'external/jquery-ui', + caret: 'external/caret', + d3: 'external/d3/d3', + autocomplete: 'external/autocomplete', + FileSaver: 'external/FileSaver' }, + baseUrl: '../src/' } +hello from test factory +here we are after test_app prerequisites +42 +Rhizi main started +rhizi: init drag-n-drop diff --git a/src/client-tests/output/test_script_jsdom.js.stderr b/src/client-tests/output/test_script_jsdom.js.stderr new file mode 100644 index 00000000..e69de29b diff --git a/src/client-tests/output/test_script_jsdom.js.stdout b/src/client-tests/output/test_script_jsdom.js.stdout new file mode 100644 index 00000000..02b09a7b --- /dev/null +++ b/src/client-tests/output/test_script_jsdom.js.stdout @@ -0,0 +1 @@ +{ foo: 'bar' } diff --git a/src/client-tests/output/test_util.js.stderr b/src/client-tests/output/test_util.js.stderr new file mode 100644 index 00000000..e69de29b diff --git a/src/client-tests/output/test_util.js.stdout b/src/client-tests/output/test_util.js.stdout new file mode 100644 index 00000000..e69de29b diff --git a/src/client-tests/run_tests.js b/src/client-tests/run_tests.js new file mode 100644 index 00000000..bfc129eb --- /dev/null +++ b/src/client-tests/run_tests.js @@ -0,0 +1,31 @@ +var fs = require('fs'); +var spawn = require('child_process').spawn; + +var tests = fs.readdirSync('.').filter(function (x) { return /^test_.*\.js$/.test(x); }); +var i; +var testname; + +function store_output(proc, args, stdout, stderr) +{ + var out = fs.openSync(stdout, 'w+'); + var err = fs.openSync(stderr, 'w+'); + var p = spawn(proc, args); + + p.stdout.on('data', function (data) { + console.log(args + ' gives ' + data.length); + fs.appendFileSync(stdout, data); + }); + p.stderr.on('data', function (data) { + console.log(args + ' gives ' + data.length + ' (err)'); + fs.appendFileSync(stderr, data); + }); + p.on('close', function (code, signal) { + console.log(proc + '(' + args.join(', ') + ') exited with ' + code); + }); + +} + +for (i in tests) { + testname = tests[i]; + store_output('/usr/bin/node', [__dirname + '/' + testname], 'output/' + testname + '.stdout', 'output/' + testname + '.stderr'); +} diff --git a/src/client-tests/test2.js b/src/client-tests/test2.js new file mode 100644 index 00000000..0f8db81f --- /dev/null +++ b/src/client-tests/test2.js @@ -0,0 +1,4 @@ +define('test2', function() { + console.log('hello from test2 factory'); + return {'me': 'not you'}; +}); diff --git a/src/client-tests/test_analyzer.js b/src/client-tests/test_analyzer.js new file mode 100644 index 00000000..441f0dde --- /dev/null +++ b/src/client-tests/test_analyzer.js @@ -0,0 +1,49 @@ +var base = require('./base'); + +var data = [ +["#a hello there #b", ["a", "b"], [["a", "b", "hello there"]], +["#a hello there2 #ba", ["a", "ba"], ["a", "ba", "hello there2"]]], +["#c and #d like #e", ["c", "d", "e"], [["c", "e", "like"], ["d", "e", "like"]]], +// issue 86 +["#f and #g and #h are cool", ["f", "g", "h", "f and g and h are cool"], [["f", "f and g and h are cool", ""], ["g", "f and g and h are cool", ""], ["h", "f and g and h are cool", ""]]], +["#i likes #j and #k", ["i", "j", "k"], ["i", "j", "likes"], ["i", "k", "likes"]], +["#q likes #r but doesn't like #l", ["q", "r", "l"], []], +]; +/* +#t and #u like #v +#w #x #y +sometimes #z and #ab aren't friends +I like to work with #a +*/ + +base.run_tests({ + done: function (errors, window) { + debugger; + var analyzeSentence = window.require('textanalysis.ui').analyzeSentence; + if (process.argv.length > 2) { + analyzeSentence(process.argv.slice(2).join(" "), true); + base.dump_graphviz(window.require('rz_core').force); + } else { + for (var k = 0; k < data.length; ++k) { + var sentence = data[k][0]; + var expected_nodes = data[k][1]; + var expected_links = data[k][2]; + base.reset(window); + console.log('analyzing ' + sentence); + analyzeSentence(sentence, true); + var nodes = base.nodes(window); + var links = base.links(window); + if (nodes != expected_nodes) { + console.log('expected: ' + expected_nodes); + console.log('got: ' + nodes.join('|')); + } + if (links != expected_links) { + console.log('expected: ' + expected_links); + console.log('got: ' + links); + } + base.dump_graphviz(window.require('rz_core').force); + } + } + process.exit(); + } +}); diff --git a/src/client-tests/test_app.js b/src/client-tests/test_app.js new file mode 100644 index 00000000..6696c1fc --- /dev/null +++ b/src/client-tests/test_app.js @@ -0,0 +1,32 @@ +// mocks just to all run_tests to succeed +if (typeof define == 'undefined') { + function define(mod, cb) { + } +} +if (typeof document == 'undefined') { + var document = {}; +} + +(function() { +var config = { + //urlArgs: "bust=" + (new Date()).getTime(), // NOTE: useful for debugging + paths: { + jquery: 'external/jquery', + 'jquery-ui': 'external/jquery-ui', + caret: 'external/caret', + 'd3': 'external/d3/d3', + autocomplete: 'external/autocomplete', + FileSaver: 'external/FileSaver', + } +} + +define('test', function() { + console.log('hello from test factory'); + return {f:function(){console.log(42);}}; +}); + +console.log('test_app: running under node'); +config.baseUrl = '../src/'; + +document.config = config; +}()); diff --git a/src/client-tests/test_globals.js b/src/client-tests/test_globals.js new file mode 100644 index 00000000..03dd1cf9 --- /dev/null +++ b/src/client-tests/test_globals.js @@ -0,0 +1,30 @@ +var base = require('./base'); +var util = require('../src/util'); + +var count; + +function object_length(obj) { + var count = 0; + for (var k in obj) { + count += 1; + } + return count; +} + +var win_before; +var win_after; + +base.run_tests({ + created: function(errors, window) { + count = object_length(window); + console.log('before fields count: ' + count); + win_before = util.set_from_object(window); + }, + done:function (errors, window) { + var new_count = object_length(window); + console.log('after fields count: ' + new_count); + console.log('new fields count: ' + (new_count - count)); + win_after = util.set_from_object(window); + console.log(util.set_diff(win_after, win_before).a_b.join(',')); + } +}); diff --git a/src/client-tests/test_require_js.js b/src/client-tests/test_require_js.js new file mode 100644 index 00000000..aa71018e --- /dev/null +++ b/src/client-tests/test_require_js.js @@ -0,0 +1,25 @@ +var jsdom = require("jsdom").jsdom; +var document = jsdom(); +var window = document.parentWindow; +var addScript = require('./test_util').addScript; + +window.console.log = console.log; +window.is_node = true; + +addScript(window, '../src/external/require.js') + .load_next('test_app.js') + .done(function () { + var config = document.config + var requirejs = window.requirejs; + var require = window.require; + console.log('callback after test_app.js loading'); + console.log(config); + require.config(config); + requirejs(['require', 'test', './test2.js', 'main'], function(require, test, test2, main) { + console.log('here we are after test_app prerequisites'); + debugger; + test.f(); + main.main(); + process.exit(); + }); + }); diff --git a/src/client-tests/test_script_jsdom.js b/src/client-tests/test_script_jsdom.js new file mode 100644 index 00000000..1ae5835f --- /dev/null +++ b/src/client-tests/test_script_jsdom.js @@ -0,0 +1,9 @@ +var jsdom = require("jsdom").jsdom; +var window = jsdom().parentWindow; + +window.__myObject = { foo: "bar" }; + +var scriptEl = window.document.createElement("script"); +scriptEl.src = "../tests/anotherScript.js"; +window.console.log = console.log; +window.document.body.appendChild(scriptEl); diff --git a/src/client-tests/test_util.js b/src/client-tests/test_util.js new file mode 100644 index 00000000..7780b0ad --- /dev/null +++ b/src/client-tests/test_util.js @@ -0,0 +1,44 @@ +function new_deffer() { + var deffer = { + cb: null, + call_cb: false, + }; + deffer.done = function(cb) { + deffer.cb = cb; + if (deffer.call_cb) { + // warning: going down callstack - should probably use setInterval + deffer.call_cb = false; // first do this to avoid endless recursion + return deffer.cb(); + } + }; + deffer.on_done = function() { + if (this.cb) { + this.call_cb = false; + this.cb(); + } else { + this.call_cb = true; + } + }; + return deffer; +} + +function addScript(window, name) { + var scriptEl = window.document.createElement("script"); + scriptEl.src = name; + var deffer = new_deffer(); + deffer.load_next = function(script) { + var sec_deffer = new_deffer(); + addScript(window, script).done( + function() { sec_deffer.on_done(); }) + return sec_deffer; + } + function onload_cb() { + console.log('addScript: loaded ' + name); + deffer.on_done(); + } + scriptEl.onload = onload_cb; + window.document.body.appendChild(scriptEl); + return deffer; +} + +exports.addScript = addScript; diff --git a/src/client-tests/util/RhiziHTTPServer.py b/src/client-tests/util/RhiziHTTPServer.py new file mode 100644 index 00000000..29e8980d --- /dev/null +++ b/src/client-tests/util/RhiziHTTPServer.py @@ -0,0 +1,78 @@ +""" +Inherits from python-2 SimpleHTTPServer (same bug exists in python3 +http.server) to fix issue where query argument is mistakenly treated as a +improperly non slash terminated path. +""" + + +import os +import BaseHTTPServer +import SimpleHTTPServer + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + + +class MyHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + + """ + Fix lack of handling for query parameters in SimpleHTTPRequestHandler + """ + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + if not path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(301) + self.send_header("Location", self.path + "/") + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + # Always read in binary mode. Opening files in text mode may cause + # newline translations, making the actual size of the content + # transmitted *less* than the content-length! + f = open(path, 'rb') + except IOError: + self.send_error(404, "File not found") + return None + try: + self.send_response(200) + self.send_header("Content-type", ctype) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + except: + f.close() + raise + +def test(HandlerClass = MyHTTPRequestHandler, + ServerClass = BaseHTTPServer.HTTPServer): + BaseHTTPServer.test(HandlerClass, ServerClass) + + +if __name__ == '__main__': + test() diff --git a/src/client-tests/weizmann.json b/src/client-tests/weizmann.json new file mode 100644 index 00000000..fac5fe08 --- /dev/null +++ b/src/client-tests/weizmann.json @@ -0,0 +1 @@ +{"nodes":[{"id":"Oren","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Hila","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Yuval","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Pareto Morphology","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Avi","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"circuits","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Pablo","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"FCD","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"animals","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"ParTI","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"genotypes","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Miri","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Jean","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Physisist","type":"skill","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Chemist","type":"skill","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Mathematician","type":"skill","state":"perm","start":0,"end":0,"status":"unknown"}],"links":[{"__src":"Yuval","__dst":"Pareto Morphology","name":"work on "},{"__src":"Oren","__dst":"Pareto Morphology","name":"work on "},{"__src":"Oren","__dst":"Pareto Morphology","name":"work on "},{"__src":"Hila","__dst":"Pareto Morphology","name":"work on "},{"__src":"Hila","__dst":"Pareto Morphology","name":"work on "},{"__src":"Avi","__dst":"Pareto Morphology","name":"works on "},{"__src":"Avi","__dst":"Pareto Morphology","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Pablo","__dst":"circuits","name":"works on "},{"__src":"Pablo","__dst":"circuits","name":"works on "},{"__src":"Pablo","__dst":"FCD","name":"works on "},{"__src":"Pablo","__dst":"animals","name":"works on "},{"__src":"Pablo","__dst":"ParTI","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Avi","__dst":"FCD","name":"works on "},{"__src":"Avi","__dst":"animals","name":"works on "},{"__src":"Yuval","__dst":"ParTI","name":"works on "},{"__src":"Yuval","__dst":"ParTI","name":"works on "},{"__src":"Hila","__dst":"circuits","name":"works on "},{"__src":"Hila","__dst":"circuits","name":"works on "},{"__src":"Hila","__dst":"genotypes","name":"works on "},{"__src":"Miri","__dst":"FCD","name":"works on "},{"__src":"Miri","__dst":"FCD","name":"works on "},{"__src":"Hila","__dst":"ParTI","name":"wrote the code and consulted with the math in "},{"__src":"Hila","__dst":"ParTI","name":"wrote the code and consulted with the math in "},{"__src":"Avi","__dst":"ParTI","name":"prepared graphical interface to "},{"__src":"Avi","__dst":"ParTI","name":"prepared graphical interface to "},{"__src":"Jean","__dst":"ParTI","name":"helped with biological interpretations in the project "},{"__src":"Jean","__dst":"ParTI","name":"helped with biological interpretations in the project "},{"__src":"Pablo","__dst":"Physisist","name":"is a "},{"__src":"Physisist","__dst":"Chemist","name":" and a "},{"__src":"Pablo","__dst":"Physisist","name":"is a "},{"__src":"Pablo","__dst":"Chemist","name":"is a "},{"__src":"Yuval","__dst":"Physisist","name":"is a "},{"__src":"Physisist","__dst":"Mathematician","name":" and a "},{"__src":"Hila","__dst":"Physisist","name":"is a "},{"__src":"Hila","__dst":"Mathematician","name":"is a "},{"__src":"Yuval","__dst":"Physisist","name":"is a "},{"__src":"Yuval","__dst":"Mathematician","name":"is a "}]} \ No newline at end of file diff --git a/src/client/ScrollTo.js b/src/client/ScrollTo.js new file mode 100644 index 00000000..a015f08c --- /dev/null +++ b/src/client/ScrollTo.js @@ -0,0 +1,17 @@ +$.fn.scrollTo = function( target, options, callback ){ + if(typeof options == 'function' && arguments.length == 2){ callback = options; options = target; } + var settings = $.extend({ + scrollTarget : target, + offsetTop : 50, + duration : 500, + easing : 'swing' + }, options); + return this.each(function(){ + var scrollPane = $(this); + var scrollTarget = (typeof settings.scrollTarget == "number") ? settings.scrollTarget : $(settings.scrollTarget); + var scrollY = (typeof scrollTarget == "number") ? scrollTarget : scrollTarget.offset().top + scrollPane.scrollTop() - parseInt(settings.offsetTop); + scrollPane.animate({scrollTop : scrollY }, parseInt(settings.duration), settings.easing, function(){ + if (typeof callback == 'function') { callback.call(this); } + }); + }); +} \ No newline at end of file diff --git a/src/client/app.js b/src/client/app.js new file mode 100644 index 00000000..39acdde7 --- /dev/null +++ b/src/client/app.js @@ -0,0 +1,31 @@ +(function() { + var lib_path = '../lib/'; + var config = { + paths: { + jquery: lib_path + 'jquery', + 'jquery-ui': lib_path + 'jquery-ui', + 'd3': lib_path + 'd3/d3', + FileSaver: lib_path + 'FileSaver', + caret: lib_path + 'caret', + autocomplete: lib_path + 'autocomplete', + Bacon: lib_path + 'Bacon', + } + } + + config.urlArgs = (typeof local_config != 'undefined') && local_config.urlArgs; + +if (window.is_node) { + // Testing path only + console.log('app: running under node'); + config.baseUrl = '../src/'; + window.rhizi_require_config = config; +} else { + // Main app path + require.config(config); + + requirejs(['main'], function(main) { + console.log('starting rhizi logic'); + main.main(); + }); +} +}()); diff --git a/src/client/buttons.js b/src/client/buttons.js new file mode 100644 index 00000000..860c8573 --- /dev/null +++ b/src/client/buttons.js @@ -0,0 +1,82 @@ +"use strict" + +define(['jquery', 'FileSaver', 'rz_core'], function ($, saveAs, rz_core) { +$('.tutorial').click(function(){}); + +var key="#47989379"; + + +$('.save a').click(function(){ + var json = rz_core.graph.save_to_json(); + console.log('saving to local storage ' + json.length + ' bytes'); + localStorage.setItem(key, json); +}); + +$('.export').click(function() { + var json = rz_core.graph.save_to_json(); + var filename = 'graph.json'; + var blob = new Blob([json], {type: 'application/json'}); + console.log('saving ' + json.length + ' bytes to ' + filename); + saveAs(blob, filename); +}); + +$('.url-copy a').click(function() { + var json = rz_core.graph.save_to_json(); + // TODO use jquery BBQ $.param({json: json}); + var encoded = document.location.origin + '/?json=' + encodeURIComponent(json); + window.prompt('Copy to clipboard: Ctrl-C, Enter (or Cmd-C for Mac)', encoded); +}); + +var really_load = function() { + if (!rz_core.graph.empty()) { + return confirm('All unsaved changes will be deleted, are you sure?'); + } + return true; +} + +$('.file-import').on('change', function(event) { + var file = event.target.files[0]; + var reader; + + if (!really_load()) { + return; + } + if (file === undefined) { + return; + } + console.log(file); + reader = new FileReader(); + reader.onload = (function(theFile) { + return function(e) { + var result = e.target.result; + if (e.target.readyState === FileReader.DONE) { + console.log('done reading ' + theFile.name); + console.log('got #' + result.length + ' bytes in ' + typeof(result)); + rz_core.load_from_json(result); + } + } + })(file); + reader.readAsText(file, "text/javascript"); +}); + +$('.local-storage-load a').click(function(){ + if (!really_load()) { + return; + } + var json_blob = localStorage.getItem(key) + rz_core.load_from_json(json_blob); +}); + +var logout_button = $('#logout-button'); +logout_button.click(function() { + $.ajax({ type: "POST", url: '/logout'}); // server should redirect back to /login +}); + +$('a.save-history').click(function() { + if (rz_core.graph.history === undefined) { + throw "History is undefined"; + } + rz_core.graph.history.save_to_file(); +}); +return {'buttons': 'nothing here'}; +}); // define diff --git a/src/client/consts.js b/src/client/consts.js new file mode 100644 index 00000000..32167811 --- /dev/null +++ b/src/client/consts.js @@ -0,0 +1,25 @@ +"use strict" + +define(function() { + + var nodetypes = ["person", "club", "skill", "interest", "third-internship-proposal", "internship"]; + + var description = { + person: 'A person in CRI - student or teacher', + club: 'A shared club or project within the CRI', + skill: 'Ability or expertise you possess', + interest: 'Scientific skill or domain expertise you wish you had', + 'third-internship-proposal': 'Create this to submit your third internship proposal', + internship: "Title of first or second internship you've done" + }; + + // TODO: enums, sometime + return { + KEYSTROKE_WHERE_EDIT_NODE: 'keystroke_where_edit_node', + KEYSTROKE_WHERE_DOCUMENT: 'keystroke_where_document', + KEYSTROKE_WHERE_TEXTANALYSIS: 'keystroke_where_textanalysis', + INPUT_WHERE_TEXTANALYSIS: 'input_where_textanalysis', + nodetypes: nodetypes, + description: description, + }; +}); diff --git a/src/client/drag_n_drop.js b/src/client/drag_n_drop.js new file mode 100644 index 00000000..da166ad4 --- /dev/null +++ b/src/client/drag_n_drop.js @@ -0,0 +1,31 @@ +define(['jquery', 'rz_core'], function($, rz_core) { + +function init() { +console.log('rhizi: init drag-n-drop'); +$(document).on('drop', function(e) { + e.stopPropagation(); + e.preventDefault(); + var files = e.originalEvent.dataTransfer.files; + var file = files[files.length - 1]; + var fr = new FileReader(); + fr.onload = function() { + if (fr.readyState != 2) { + console.log('drop: error: reading from file failed'); + } else { + console.log('loading dropped file'); + rz_core.load_from_json(fr.result); + } + } + fr.readAsText(file); + return false; +}); +$(document).on('dragover', function (e) +{ + e.stopPropagation(); + e.preventDefault(); + return false; +}); +}; +return {'init': init }; + +}); // define diff --git a/src/client/history.js b/src/client/history.js new file mode 100644 index 00000000..ab864d1c --- /dev/null +++ b/src/client/history.js @@ -0,0 +1,149 @@ +"use strict" +// Once upon a time we shall have a versioned property graph from which history +// will be one extractable aspect, much like a git for graphs. Now we just have +// a plain list of events for a specific user. + +// Enums and chrome don't play along well. Object.freeze I guess? actually rhizi code cuases +// exceptions but that shouldn't break the console, as evidenced by the '__commandLineAPI is not defined' +// error below. +// +// Uncaught TypeError: Can't add property addednodes, object is not extensible rz_core.js:4 +// Uncaught TypeError: Can't add property text, object is not extensible textanalysis.js:3 +// Uncaught TypeError: Can't add property key, object is not extensible buttons.js:5 +// Uncaught ReferenceError: sentence is not defined robot.js:11 +// Resource interpreted as Font but transferred with MIME type application/font-sfnt: "http://localhost:8000/external/Lato300.ttf". jquery.js:2 +// > document +// ReferenceError: __commandLineAPI is not defined +//var ActionEnum = Enum(); + +define(['jquery', 'FileSaver', 'consts', 'rz_bus'], + function($, saveAs, consts, rz_bus) { + +/* user - username (string) + * svg - svg element for catching zoom events (jquery DOMNode wrapper) + */ +function History(user, graph, transform_element) { + var that = this; + this.records = []; + this.user = user; + this.transform_element = transform_element; + graph.diffBus.onValue(function (obj) { + return that.record_graph_diff(obj) + }); + rz_bus.ui_key.onValue(that.record_keystrokes.bind(that)); + rz_bus.ui_input.onValue(that.record_input.bind(that)); + // XXX create zoom behavior - then proof to event name change + $(window).on('wheel.history', function(obj) { + that.record_zoom(obj); + return true; + }); +} + +var ACTION_KEYSTROKES = 'ACTION_KEYSTROKES'; +var ACTION_INPUT = 'ACTION_INPUT'; +var ACTION_GRAPH_DIFF = 'ACTION_GRAPH_DIFF'; +var ACTION_ZOOM = 'ACTION_ZOOM'; + +var KEYSTROKE_WHERE_TEXTANALYSIS = 'KEYSTROKE_WHERE_TEXTANALYSIS'; +var KEYSTROKE_WHERE_DOCUMENT = 'KEYSTROKE_WHERE_DOCUMENT'; +var KEYSTROKE_WHERE_EDIT_NODE = 'KEYSTROKE_WHERE_EDIT_NODE'; + +History.prototype.record = function(action, d) +{ + if (d === undefined || action === undefined) { + throw "Invalid arguments"; + } + d['action'] = action; + d['user'] = this.user; + d['timestamp'] = new Date(); + this.records.push(d); + $('.history-timeline').html('
' + JSON.stringify(d) + '
'); +}; + +function svg_extract_translate_and_scale(e) +{ + // See: http://stackoverflow.com/questions/10349811/how-to-manipulate-translate-transforms-on-a-svg-element-with-javascript-in-chrom + // Using the regexp option right now, did only firefox testing 36 + var str = e.attributes['transform'].value; + var parts = /translate\(\s*([^\s,)]+)[ ,]([^\s,)]+)/.exec(str); + var scale = /scale\(\s*([^\s)]+)\)/.exec(str); + if (scale) { + var x = parts[1], y = parts[2]; + return {scale:+scale[1], translate: [+x, +y]}; + } else { + return {scale:1.0, translate: [0.0, 0.0]}; + } +} + +History.prototype.record_zoom = function(d) +{ + var transform = svg_extract_translate_and_scale(this.transform_element); + + if (transform === undefined) { + console.log('record_zoom: bug: transform_element has no transform attribute'); + return; + } + this.record(ACTION_ZOOM, {transform: transform}); +} + +History.prototype.save_to_file = function() +{ + var json = JSON.stringify(this.records, function (k, v) { return v; }, 2); + + saveAs(new Blob([json], {type: 'application/json'}), 'history.json'); +}; + +History.prototype.clear = function() +{ + this.records = []; +} + +History.prototype.record_graph_diff = function(obj) +{ + this.record(ACTION_GRAPH_DIFF, { + nodes: {add: obj.nodes && obj.nodes.add, remove: obj.nodes && obj.nodes.remove, + change: obj.nodes && obj.nodes.change}, + links: {add: obj.links && obj.links.add, remove: obj.links && obj.links.remove, + change: obj.links && obj.links.change}, + }); +} + +History.prototype.record_keystrokes = function(obj) +{ + var where = obj.where, + keys = obj.keys; + + if (where === undefined || keys === undefined || keys.length === undefined || + keys.length <= 0) { + console.log("invalid arguments"); + return; + } + keys = keys.filter(function(k) { return k !== undefined; }); + if (keys.length == 0) { + return; + } + this.record(ACTION_KEYSTROKES, { + keys: keys, + where: where + }); +} + +History.prototype.record_input = function(obj) +{ + var where = obj.where, + input = obj.input; + + if (where === undefined || input === undefined || input.length === undefined || typeof input !== 'string') { + console.log('invalid arguments'); + return; + } + this.record(ACTION_INPUT, {where: where, input: input}); +} + +return { + History:History, + KEYSTROKE_WHERE_TEXTANALYSIS:KEYSTROKE_WHERE_TEXTANALYSIS, + KEYSTROKE_WHERE_DOCUMENT:KEYSTROKE_WHERE_DOCUMENT, + KEYSTROKE_WHERE_EDIT_NODE:KEYSTROKE_WHERE_EDIT_NODE +}; +}); // define diff --git a/src/client/main.js b/src/client/main.js new file mode 100644 index 00000000..863d0440 --- /dev/null +++ b/src/client/main.js @@ -0,0 +1,94 @@ +define(['textanalysis.ui', 'textanalysis', 'buttons', 'history', 'drag_n_drop', 'robot', 'model/core', 'rz_config', 'rz_core', 'view/selection', 'util', 'view/completer'], +function(textanalysis_ui, textanalysis, buttons, history, drag_n_drop, robot, model_core, rz_config, rz_core, selection, util, completer) { + + function expand(obj){ + if (!obj.savesize) { + obj.savesize = obj.size; + } + obj.size = Math.max(obj.savesize, obj.value.length); + } + + this.main = function() { + var json, + search = $('#search'), + search_completer = completer(search, $('#search-suggestion'), + {triggerStart:' ', triggerEnd:' '}); + + console.log('Rhizi main started'); + search_completer.options.plug(textanalysis.suggestions_options); + drag_n_drop.init(); + $('#editname').onkeyup = function() { expand(this); }; + $('#editlinkname').onkeyup = function() { expand(this); }; + $('#textanalyser').onkeyup = function() { expand(this); }; + + textanalysis_ui.main(); + + json = util.getParameterByName('json'); + if (json) { + rz_core.load_from_json(json); + } + if (util.getParameterByName('debug')) { + $(document.body).addClass('debug'); + rz_core.graph.set_user('fakeuser'); + } + + document.body.onkeyup = function(e) { + var key = (e.key || (e.charCode && String.fromCharCode(e.charCode)) + || (e.which && String.fromCharCode(e.which))).toLowerCase(); + + if (e.altKey && e.ctrlKey && key == 'i') { + $('#textanalyser').focus(); + } + if (e.altKey && e.ctrlKey && key == 'o') { + search.focus(); + } + if (e.ctrlKey && key == 'z' && e.target.nodeName !== 'INPUT') { + // TODO: rz_core.graph.undo(); + } + }; + // TODO: move me somewhere + function search_on_submit() { + var text = search[0].value.trim(), + r; + + try { + r = new RegExp(text.replace(/ /, '|'), 'i'); + } catch (e) { + return; // don't clear selection either + } + if (text.length > 0) { + selection.byVisitors(function (n) { return n.name.match(r); }); + } else { + selection.clear(); + } + rz_core.update_view__graph(false); + }; + search.on('input', search_on_submit); + search.on('keydown', function(e) { + if (e.which == 13 && !search_completer.handleEnter()) { + e.preventDefault(); + search_on_submit(e); + return false; + } + return undefined; + }); + + var intro_task_elem = $('#intro-task'); + // TODO: messages (why tasks?) - this one is special but we want them to be handled in their own file. + if (!localStorage.intro_task_hide) { + intro_task_elem.show(); + } + $('#intro-task .task-close-button').click(function(e) { + localStorage.intro_task_hide = true; + intro_task_elem.hide(); + }); + + // TODO: interaction between the hack above and this + model_core.init(rz_config); + textanalysis.init(rz_core.graph); + } + + return { + main: main }; + } +); diff --git a/src/client/model/core.js b/src/client/model/core.js new file mode 100644 index 00000000..5c643ea1 --- /dev/null +++ b/src/client/model/core.js @@ -0,0 +1,187 @@ +"use strict" + +/** + * core model module - currently unused + */ +define(['util'], function(util) { + + /** + * return a random id + */ + var random_id; + + var random_id__hash = function() { + return Math.random().toString(36).substring(2, 10); + } + + var random_id__seq = function () { + var id = 0; + function get_next() { + var next = id; + id += 1; + return next; + } + return get_next; + } + + function random_node_name() { + return random_id__hash(); + } + + function init(config){ + if (config['rand_id_generator'] == 'hash') { + random_id = random_id__hash; + } + if (config['rand_id_generator'] == 'seq') { + random_id = random_id__seq(); + } + } + + function Node() { + } + Node.prototype.equals = function(other_node){ + return this.id == other_node.id; + } + + function Link() { + } + // adapte Link to force_layoutL create __src,__dst aliases + Link.prototype.__defineGetter__('source', function(){ + return this.__src; + }); + Link.prototype.__defineGetter__('target', function(){ + return this.__dst; + }); + + /** + * the most flexible way to create a node: - perform spec field validation - + * fill-in missing spec fields + */ + function create_node_from_spec(node_spec) { + var ret = new Node(); + + if (undefined != node_spec.id) { + // reuse id if present + __set_obj_id(ret, node_spec.id); + } + + util.assert(undefined != node_spec.name, 'create_node_from_spec: name missing'); + + ret.name = node_spec.name; + + // type + if (undefined == node_spec.type) { + console.debug('create_node_from_spec: undefined type, falling back to \'empty\''); + node_spec.type = 'empty'; + } + ret.type = node_spec.type; + + // status + ret.status = node_spec.status || 'unknown'; + + // visual + ret.x = node_spec.x; + ret.y = node_spec.y; + + // other + ret.state = node_spec.state; + ret.url = node_spec.url; + ret.start = node_spec.start; + ret.end = node_spec.end; + + return ret; + } + + function __set_obj_id(obj, id) { + Object.defineProperty(obj, "id", { + value: id, + enumerable: true, + writable: false + }); + } + + /** + * @param node_spec: id must not be defined + */ + function create_node__set_random_id(node_spec) { + if (undefined == node_spec) { + node_spec = {}; + } + + var ret = create_node_from_spec(node_spec); + + util.assert(undefined == ret.id); // id must not be defined in spec + __set_obj_id(ret, random_id()); + + return ret; + } + + function create_link__set_random_id(src, dst, link_spec) { + var ret = create_link_from_spec(src, dst, link_spec); + __set_obj_id(ret, random_id()); + return ret; + } + + /** + * determine if nodes are equal by name + * + * @param other_node + * @returns {Boolean} + */ + Node.prototype.equal_by_name = function(other) { + ret = this.name.toLowerCase() == other.name.toLowerCase(); + if (false == ret) { + console.debug(this.id + ' != ' + other.id); + } + return ret; + } + + function create_link_from_spec(src, dst, link_spec) { + var ret = new Link(); + + if (undefined != link_spec.id) { + // reuse id if present + __set_obj_id(ret, link_spec.id); + } + + util.assert(undefined != src, 'create_link_from_spec: src missing'); + util.assert(undefined != dst, 'create_link_from_spec: dst missing'); + util.assert(undefined != src.id, 'create_link_from_spec: src missing id'); + util.assert(undefined != dst.id, 'create_link_from_spec: dst missing id'); + util.assert(undefined != link_spec.name, 'create_link_from_spec: name missing, unable to deduce type'); + + ret.__src = src; + ret.__dst = dst; + ret.__type = link_spec.name; + + if (undefined == link_spec.name){ + console.warn('create_link_from_spec: name: ' + link_spec.name); + link_spec.name = ""; + } + ret.name = link_spec.name.trim(); + + ret.state = link_spec.state; + return ret; + } + + /** + * determine if links are equal by ID + * + * @param other_node + * @returns {Boolean} + */ + Link.prototype.equal_by_id = function(other) { + return this.id.toLowerCase() == other.id.toLowerCase(); + } + + return { + init : init, + Node: Node, // allow model adaptation + Link: Link, // allow model adaptation + random_node_name : random_node_name, + create_node_from_spec : create_node_from_spec, + create_node__set_random_id : create_node__set_random_id, + create_link_from_spec : create_link_from_spec, + create_link__set_random_id : create_link__set_random_id, + }; +}); diff --git a/src/client/model/diff.js b/src/client/model/diff.js new file mode 100644 index 00000000..8903b161 --- /dev/null +++ b/src/client/model/diff.js @@ -0,0 +1,187 @@ +"use strict" + +/** + * Diff module + */ +define([], + function() { + + /** + * A set of diff objects + */ + function Diff_Set(obj_spec) { + this.__diff_set_topo = []; + this.__diff_set_attr = []; + this.__diff_set_vis = []; + } + Diff_Set.prototype.add_diff_obj = function(diff_obj) { + if (diff_obj instanceof Topo_Diff) { + this.__diff_set_topo.push(diff_obj); + } + if (diff_obj instanceof Attr_Diff) { + this.__diff_set_attr.push(diff_obj); + } + if (diff_obj instanceof Vis_Diff) { + this.__diff_set_vis.push(diff_obj); + } + } + + /** + * Topological diff object + */ + function Topo_Diff(obj_spec) { + + this.link_set_rm = obj_spec.link_set_rm; + this.node_set_rm = obj_spec.node_set_rm; + this.node_set_add = obj_spec.node_set_add; + this.link_set_add = obj_spec.link_set_add; + + } + Topo_Diff.prototype.for_each_node_add = function(callback, this_arg) { + this.node_set_add.forEach(callback, this_arg); + } + + Topo_Diff.prototype.for_each_node_rm = function(callback, this_arg) { + this.node_set_rm.forEach(callback, this_arg); + } + + Topo_Diff.prototype.for_each_link_add = function(callback, this_arg) { + this.link_set_add.forEach(callback, this_arg); + } + + Topo_Diff.prototype.for_each_link_rm = function(callback, this_arg) { + this.link_set_rm.forEach(callback, this_arg); + } + + /** + * Attribute diff object, organized by type, where currently + * node,link types are supported + */ + function Attr_Diff(obj_spec) { + this.__type_node = {}; + this.__type_link = {}; + } + + Attr_Diff.prototype.init_attr_diff = function(type_name, id) { + + if ('node' != type_name && 'link' != type_name) { + console.error('attempt to init attribute diff for unsupported type: ' + type_name); + return; + } + + var type_field = '__type_' + type_name; + this[type_field][id] = { + '__attr_write' : {}, + '__attr_remove' : [] + }; + + return this; + } + + Attr_Diff.prototype.init_attr_diff_node = function(id) { + return this.init_attr_diff('node', id); + } + + Attr_Diff.prototype.init_attr_diff_link = function(id) { + return this.init_attr_diff('link', id); + } + + Attr_Diff.prototype.add_node_attr_write = function(n_id, attr_name, + attr_val) { + + if (undefined == this.__type_node[n_id]) { + this.init_attr_diff_node(n_id); + } + this.__type_node[n_id].__attr_write[attr_name] = attr_val; + return this; + } + + Attr_Diff.prototype.add_node_attr_rm = function(n_id, attr_name) { + if (undefined == this[n_id]) { + this.init_attr_diff(n_id); + } + this.__type_node[n_id].__attr_remove.push(attr_name); + return this; + } + + Attr_Diff.prototype.add_link_attr_write = function(l_id, attr_name, + attr_val) { + + if (undefined == this.__type_link[l_id]) { + this.init_attr_diff_link(l_id); + } + this.__type_link[l_id].__attr_write[attr_name] = attr_val; + return this; + } + + Attr_Diff.prototype.add_link_attr_rm = function(l_id, attr_name) { + if (undefined == this[l_id]) { + this.init_attr_diff(l_id); + } + this.__type_link[l_id].__attr_remove.push(attr_name); + return this; + } + + /** + * Visual diff object expressing any visual change to the state of a + * particular visualization type. + * + * @obj_spec if none is passed a default topo_diff is constructed + * with node,link add sets + */ + function Vis_Diff(obj_spec) { + } + + function new_topo_diff(obj_spec) { + /* + * validate obj_spec + */ + var ret; + if (undefined == obj_spec) { + obj_spec = { + node_set_add : [], + link_set_add : [], + } + ret = new Topo_Diff(obj_spec); + } else { + ret = new Topo_Diff(obj_spec); + } + return ret; + } + + function new_attr_diff(obj_spec) { + /* + * validate obj_spec + */ + // TODO + var ret = new Attr_Diff(obj_spec); + ret.__type_node = {}; // id-to-obj map + ret.__type_link = {}; // id-to-obj map + return ret; + } + + function new_vis_diff(obj_spec) { + /* + * validate obj_spec + */ + // TODO + var ret = new Vis_Diff(obj_spec); + return ret; + } + + function new_diff_set(obj_spec) { + /* + * validate obj_spec + */ + // TODO + var ret = new Diff_Set(obj_spec); + return ret; + } + + return { + new_topo_diff : new_topo_diff, + new_attr_diff : new_attr_diff, + new_vis_diff : new_vis_diff, + new_diff_set : new_diff_set, + } + }); \ No newline at end of file diff --git a/src/client/model/graph.js b/src/client/model/graph.js new file mode 100644 index 00000000..b3c8e33f --- /dev/null +++ b/src/client/model/graph.js @@ -0,0 +1,873 @@ +"use strict" + +define(['Bacon', 'consts', 'util', 'model/core', 'model/util', 'model/diff', 'rz_api_backend', 'rz_api_mesh', 'history', 'rz_bus', 'rz_config'], +function (Bacon, consts, util, model_core, model_util, model_diff, rz_api_backend, rz_api_mesh, history, rz_bus, rz_config) { + +var debug = false; + +function Graph() { + + var nodes = [], + id_to_node_map = {}, + links = [], + diffBus = new Bacon.Bus(); + + this.diffBus = diffBus; + + /** + * add node if no previous node is present whose id equals that of the node being added + * + * @return node if node was actually added + */ + this.addNode = function(spec) { + var node = this.__addNode(spec); + if (node) { + return node; + } + } + + /** + * Inner implementation + * + * @param notify whether or not a presenter notification will be sent, default = true + */ + function __addNode(spec, notify, peer_notify) { + var existing_node, + node; + + notify = undefined === notify ? true : notify; + peer_notify = undefined === peer_notify ? true : peer_notify; + + if (undefined == spec.id) { + existing_node = findNodeByName(spec.name) + if (existing_node){ + return existing_node; + } else { + node = model_core.create_node__set_random_id(spec); + if (debug) { + if ('bubble' != node.type){ + console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\' (bubble)'); + }else { + console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\''); + } + } + } + } else { + node = model_core.create_node_from_spec(spec); + } + + existing_node = find_node__by_id(node.id); + if (existing_node) { + console.log('__addNode: id collision: existing-node.id: \'' + existing_node.id + '\', ' + 'new-node.id: \'' + node.id + '\''); + return existing_node; + } + + util.assert(undefined != node.id, '__addNode: node id missing'); + nodes.push(node); + id_to_node_map[node.id] = node; + console.log('__addNode: node added: id: ' + node.id); + + if (rz_config.backend_enabled && peer_notify){ + var topo_diff = model_diff.new_topo_diff({ + node_set_add : [node].map(model_util.adapt_format_write_node), + }); + var on_success = function(){ + // FIXME: handle possible outcomes: + // - id merge: node already exists -> update id + // - link-merge: node already exists -> merge links, recurse? + }; + var on_error = function(){ + // TODO: add problem emblem to node + }; + rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error); + } + + if (notify) { + diffBus.push({nodes: {add: [node]}}); + } + + return node; + } + this.__addNode = __addNode; + + this._remove_node_set = function(ns, peer_notify) { + + peer_notify = undefined === peer_notify ? true : peer_notify; + + var cascade_link_rm_set = []; // track cascading link removals + for (var j = 0; j < ns.length; j++) { + var n = ns[j]; + var i = 0; + while (i < links.length) { + var link = links[i]; + if ((link['__src'].equals(n)) || (link['__dst'].equals(n))) { // compare by id + links.splice(i, 1); + cascade_link_rm_set.push(link); + } + else { + i++; + } + } + var index = findNodeIndex(n.id, n.state); + if (index !== undefined) { + nodes.splice(index, 1); + + util.assert(undefined != n.id, '_remove_node_set: node id missing'); + delete id_to_node_map[n.id]; + } + } + + cascade_link_rm_set.forEach(function(n){ + console.log('_remove_node_set: removed node: id: ' + n.id); + }); + + if (rz_config.backend_enabled && peer_notify){ + var topo_diff = model_diff.new_topo_diff({ + node_set_rm : ns.map(function(n){ return n.id; }), + link_set_rm : cascade_link_rm_set.map(function(l){ return l.id; }), + }); + var on_success = function(){ + // FIXME: handle possible outcomes: + // - rm cascade of connected links + }; + var on_error = function(){ + // TODO: add problem emblem to node + }; + rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error); + } + + if (ns.length > 0) { + diffBus.push({nodes: {removed: ns.map(function(n) { return n.id; })}}); + } + } + + this.removeNode = function(id) { + var n = find_node__by_id(id); + this._remove_node_set([n]); + } + + this.removeNodes = function(n_filer) { + var ns = find_node_set_by_filer(n_filer); + this._remove_node_set(ns); + } + + /** + * + * getConnectedNodesAndLinks + * + * @id + * @state - defines the starting node (must have id and state) + * @d - depth defining connected component. If -1 returns the entire connected component. (can be the whole graph) + * + * NOTE: chainlinks are treated specially, they don't count for distance. So all their decendants will be added. + * + * NOTE: temp state nodes (n.state === 'temp') are ignored. + * + * @return - { + * 'node': [node] + * 'link': [link] + * } + * + * TODO: rewrite using efficient data structure. Right now iterates over everything + * TODO: implement for d !== 1 + * + */ + this.getConnectedNodesAndLinks = function(chosen_nodes, d) { + var ret = {'nodes':[], 'links':[]}; + + function addNode(node) { + if (chosen_nodes.filter(function (n) { return n.id == node.id; }).length == 1) { + return; + } + ret.nodes.push(node); + } + function same(n1, n2) { + // XXX: using name comparison because one of the nodes might be stale + return compareNames(n1.name, n2.name); + } + + if (chosen_nodes === undefined) { + console.log('getConnectedNodesAndLinks: bug: called with undefined node'); + return; + } + if (d !== 1) { + console.log('getConnectedNodesAndLinks: bug: not implemented for d == ' + d); + } + d = d || 1; + + if (chosen_nodes.length === undefined) { + console.log('getConnectedNodesAndLinks: expected array'); + } + + links.forEach(function(link) { + chosen_nodes.forEach(function (n) { + var adjacentnode; + if (same(link.__src, n)) { + adjacentnode = find_node__by_id(link.__dst.id); + if (adjacentnode.state !== "temp") { + addNode({type: 'exit', node: adjacentnode}); + } + ret.links.push({type: 'exit', link: link}); + if (link.__dst.type === "chainlink") { + links.forEach(function(link2) { + if (link.__dst.id === link2.__dst.id && + link2.__dst.type === "chainlink" && + link2.__dst.state !== "temp") { + adjacentnode = find_node__by_id(link2.__src.id); + if (adjacentnode.state !== "temp") { + addNode({type: 'enter', node: adjacentnode}); + } + ret.links.push({type: 'enter', link: link2}); + } + }); + } + } + if (same(link.__dst, n)) { + adjacentnode = find_node__by_id(link.__src.id); + if (adjacentnode.state !== "temp") { + addNode({type: 'enter', node: adjacentnode}); + } + ret.links.push({type: 'enter', link: link}); + } + }); + }); + return ret; + } + + /* compareSubset: + * state: one of the optional states that defines a subgraph + * new_nodes: array of objects with name + * new_links: array of length two arrays [source_name, target_name] + * returns: true if current and new graph are homomorphic up to + * a single node id change. false otherwise + */ + this.compareSubset = function(state, new_nodes, new_links) { + // Note: the nodes include a state=='temp', type=='bubble' node + // but it's ok since it exists both in new_nodes and in state_nodes + var state_nodes = findNodes(null, state).filter(function (nd) { + return nd.type !== 'bubble'; + }); + var state_links = findLinks(state).map(function(link) { + return [link.__src.name, link.__dst.name]; + }).sort(); + var k; + var state_source, state_target, new_source, new_target; + var changed_nodes; + var verbose = false; // XXX should be global. + var set_old_name, set_new_name; + + new_nodes.map(function (f) { + if (!f.name) { + console.log('missing name on node. node follows'); + console.log(f); + } + }); + new_nodes.sort(); + new_links.sort(); + if (new_nodes.length != state_nodes.length || new_links.length != state_links.length) { + if (verbose) { + console.log('not same size: new/old ' + new_nodes.length + ' / ' + state_nodes.length + '; ' + + new_links.length + ' / ' + state_links.length); + } + return {graph_same: false}; + } + changed_nodes = util.set_diff(util.set_from_array(state_nodes.map(function(d) { return d.name; })), + util.set_from_array(new_nodes.map(function (f) { return f.name; }))); + // we allow any number of changed nodes as long as we it is 1 or 2 :) + if (changed_nodes.a_b.length > 2) { + if (verbose) { + console.log('changed too many nodes'); + console.log(changed_nodes); + } + return {graph_same: false}; + } + set_old_name = util.set_from_array(changed_nodes.a_b); + set_new_name = util.set_from_array(changed_nodes.b_a); + for (k = 0 ; k < state_links.length ; ++k) { + state_source = state_links[k][0]; + state_target = state_links[k][1]; + new_source = new_links[k][0]; + new_target = new_links[k][1]; + if ((state_source !== new_source && + !(state_source in set_old_name && new_source in set_new_name)) + || + (state_target !== new_target && + !(state_target in set_old_name && new_target in set_new_name))) { + if (verbose) { + console.log('not same link: ' + + state_source + '->' + state_target + ' != ' + + new_source + '->' + new_target); + console.log('state_source === new_source: ' + String(state_source === new_source)); + console.log('state_target === new_target: ' + String(state_target === new_target)); + console.log(set_old_name); + console.log(set_new_name); + } + return {graph_same: false}; + } + } + return {graph_same: true, old_name: changed_nodes.a_b, new_name: changed_nodes.b_a}; + } + + this.addLinkByName = function(src_name, dst_name, name, state, drop_conjugator_links) { + + var src = findNodeByName(src_name), + dst = findNodeByName(dst_name), + src_id = src ? src.id : null, + dst_id = dst ? dst.id : null; + + if (src_id === null || dst_id === null) { + console.log('error: link of missing nodes: ' + src_name + ' (' + src_id + ') -> ' + + dst_name + ' (' + dst_id + ')'); + return; + } + + var link = model_core.create_link__set_random_id(src, dst, { name: name, + state: state }); + this.addLink(link); + } + + function addLink(link, peer_notify) { + + util.assert(link instanceof model_core.Link); + + peer_notify = undefined === peer_notify ? true : peer_notify; + + var existing_link = findLink(link.__src.id, link.__dst.id, link.name); + + if (undefined == existing_link) { + + links.push(link); + + if (rz_config.backend_enabled && peer_notify){ + var topo_diff = model_diff.new_topo_diff({ + link_set_add : [link].map(model_util.adapt_format_write_link), + }); + var on_success = function(){ + // FIXME: handle possible outcomes: + // - id merge: link already exists -> update id + // - attr-merge: link already exists -> merge attrs + }; + var on_error = function(){ + // TODO: add problem emblem to node + }; + rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error); + } + + diffBus.push({links: {add: [link]}}); + } else { + existing_link.name = link.name; + existing_link.state = link.state; + } + } + this.addLink = addLink; + + this.editLink = function(src_id, dst_id, newname, newstate) { + var link = findLink(src_id, dst_id, newname); + + if (link === undefined) { + return; + } + link.name = newname; + if (newstate !== undefined) { + link.state = newstate; + } + rz_bus.names.push([newname]); + } + + this.editLinkTarget = function(src_id, dst_id, new_dst_id) { + var link = findLink(src_id, dst_id, null); + if (link !== undefined) { + link.__dst = find_node__by_id(new_dst_id); + + } else { + + } + } + + this.update_node = function(node, new_node_spec, on_success, on_error) { + util.assert(node instanceof model_core.Node); + + if (rz_config.backend_enabled){ + + if (node.name != new_node_spec.name){ + /* + * handle name update collision: suggest removal first + */ + var n_eq_name = findNodeByName(new_node_spec.name); + if (undefined != n_eq_name) { + // delete colliding node on rename + console.warn('update_node: name collision blocked due to node rename'); + undefined != on_error && on_error(); + return; + } + + node['name'] = new_node_spec['name']; // [!] may still fail due to server NAK + } + + var attr_diff = model_diff.new_attr_diff(); + for (var key in new_node_spec){ + attr_diff.add_node_attr_write(node.id, key, new_node_spec[key]); + } + + var on_ajax_success = function(id_to_node_map){ + var node_id = node.id; // original node id + if (id_to_node_map[node_id].id != node_id){ + // TODO: handle incoming ID update + util.assert(false, 'update_node: id attr change'); + } + + var ret_node = id_to_node_map[node_id]; + for (var key in ret_node){ + if ('name' == key || 'id' == key){ + continue; + } + node[key] = ret_node[key]; + } + + // TODO: handle NAK: add problem emblem to node + on_success(); + }; + + var on_ajax_error = function(){ + }; + + rz_api_backend.commit_diff__attr(attr_diff, on_ajax_success, on_ajax_error); + } + } + + this.editNameByName = function(old_name, new_name) { + var node = findNodeByName(old_name); + + if (node === undefined) { + console.log('editNameByName: error: cannot find node with name ' + old_name); + return; + } + return this.editName(node.id, new_name); // TODO: introduce Node class (yes Amir, I'm now down with that). + } + + this.editName = function(id, new_name) { + var n_eq_name = findNodeByName(new_name); + var n_eq_id = find_node__by_id(id); + var acceptReplace=true; + + if (n_eq_id === undefined) { + return; + } + if (n_eq_id.name == new_name) { + return; + } + if (n_eq_name !== undefined && n_eq_id.state !== 'temp' && !compareNames(n_eq_id.name, new_name)) { + acceptReplace = confirm('"' + n_eq_name.name + '" will replace "' + n_eq_id.name + '", are you sure?'); + if (acceptReplace){ + for (var i = 0; i < links.length; i++) { + if (links[i].__src === n_eq_id) { + links[i].__src = n_eq_name; + } + if (links[i].__dst === n_eq_id) { + links[i].__dst = n_eq_name; + } + } + this.removeNode(n_eq_id.id); + } + } else { + n_eq_id.name = new_name; + } + } + + this.editDates = function(id, state, start, end) { + var n = find_node__by_id(id); + if (state != n.state){ + return; + } + if ((n !== undefined)) { + n.start = start; + n.end = end; + } + } + + /** + * editType: + * + * @return true if type changed + */ + this.editType = function(id, state, newtype) { + return this._editProperty(id, state, 'type', newtype); + } + + this.editURL = function(id, state, url) { + return this._editProperty(id, state, 'url', url); + } + + this._editProperty = function(id, state, prop, value) { + var n = find_node__by_id(id); + if (state != n.state){ + return false; + } + + if ((n === undefined)) { + return false; + } + n[prop] = value; + return true; + } + + this.editStatus = function(id, state, status) { + return this._editProperty(id, state, 'status', status); + } + + this.editState = function(id, state, newstate) { + return this._editProperty(id, state, 'state', newstate); + } + + this.findCoordinates = function(id) { + var n = find_node__by_id(id); + if ((index !== undefined)) { + $('.typeselection').css('top', n.y - 90); + $('.typeselection').css('left', n.x - 230); + } + } + + this.removeLink = function(link) { + var i; + + for (i = 0 ; i < links.length; ++i) { + if (link.id !== undefined) { + if (link.id === links[i].id) { + links.splice(i, 1); + return; + } + } else { + if (link.__src.id === links[i].__src.id && link.__dst.id === links[i].__dst.id) { + links.splice(i, 1); + return; + } + } + } + console.log('bug: attempt to remove non existant link'); + } + + this.removeLinks = function(state) { + var id = null; + var ls = findLinks(state); + for (var j = 0; j < ls.length; j++) { + var l = ls[j]; + var i = 0; + while (i < links.length) { + if (links[i] === l) links.splice(i, 1); + else i++; + } + } + } + + var findLink = function(src_id, dst_id, name) { + for (var i = 0; i < links.length; i++) { + if (links[i].__src.id === src_id && links[i].__dst.id === dst_id) { + return links[i]; + } + } + } + + var findLinks = function(state) { + var foundLinks = []; + for (var i = 0; i < links.length; i++) { + if (links[i].state == state) { + foundLinks.push(links[i]); + } + } + return foundLinks; + } + + var compareNames = function(name1, name2) { + return name1.toLowerCase() === name2.toLowerCase(); + }; + + var hasNodeByName = function(name, state) { + return nodes.filter(function (n) { + return compareNames(n.name, name) && n.state === state; + }).length > 0; + } + this.hasNodeByName = hasNodeByName; + + var hasNodeByNameAndNotState = function(name, state) { + return nodes.filter(function(n) { + return compareNames(n.name, name) && n.state !== state; + }).length > 0; + } + this.hasNodeByNameAndNotState = hasNodeByNameAndNotState; + + var hasNode = function(id, state) { + var i; + + for (i = 0 ; i < nodes.length; ++i) { + if (nodes[i].id === id && nodes[i].state === state) { + return true; + } + } + return false; + } + this.hasNode = hasNode; + + /** + * return node whose id matches the given id or undefined if no node was found + */ + var find_node__by_id = function(id) { + return id_to_node_map[id]; + } + + /** + * @param filer: must return true in order for node to be included in the returned set + */ + var find_node_set_by_filer = function(filter) { + var ret = []; + nodes.map(function(n){ + if (true == filter(n)){ + ret.push(n); + } + }); + return ret; + } + + var findNodeByName = function(name) { + for (var i = 0 ; i < nodes.length ; ++i) { + if (compareNames(nodes[i].name, name)) { + return nodes[i]; + } + } + } + + var findNodes = function(id, state) { + // id=id.toLowerCase(); + var foundNodes = []; + for (var i = 0; i < nodes.length; i++) { + if ((id && nodes[i].id === id) || (state && nodes[i].state === state)) + foundNodes.push(nodes[i]); + } + return foundNodes; + } + + var findNodeIndex = function(id, state) { + for (var i = 0; i < nodes.length; i++) { + if ((id && nodes[i].id === id) || (state && nodes[i].state === state)) + return i; + }; + } + + function clear() { + nodes.length = 0; + links.length = 0; + } + this.clear = clear; + + function empty() { + return nodes.length == 0 && links.length == 0; + } + this.empty = empty; + + // @ajax-trans + this.commit_diff_set = function (diff_set) { + + function on_success(data){ + console.log('commit_diff_set:on_success: TODO impl'); + } + + rz_api_mesh.broadcast_possible_next_diff_block(diff_set); + } + + /** + * perform initial DB load from backend + * + * @param on_success: should be used by MVP presentors to trigger UI update + */ + // @ajax-trans + function load_from_backend(on_success) { + + function on_success__ajax(data){ + var n_set = []; // added node set + var l_set = []; // added link set + var len; + + data['node_set'].map(function(n_spec) { + n_spec = model_util.adapt_format_read_node(n_spec); + + util.assert(undefined != n_spec.id, 'load_from_backend: n_spec missing id'); + + var n = __addNode(n_spec, false, false); + n_set.push(n); + }); + + data['link_set'].map(function(l_spec){ + var l_ptr = model_util.adapt_format_read_link_ptr(l_spec); + + util.assert(undefined != l_ptr.id, 'load_from_backend: l_ptr missing id'); + + // resolve link ptr + var src = find_node__by_id(l_ptr.__src_id), + dst = find_node__by_id(l_ptr.__dst_id); + + // cleanup & reuse as link_spec + delete l_ptr.__src_id; + delete l_ptr.__dst_id; + var link_spec = l_ptr; + var link = model_core.create_link_from_spec(src, dst, link_spec); + var l = addLink(link, false); + l_set.push(l); + }); + + undefined != on_success && on_success() + } + + rz_api_backend.clone(0, on_success__ajax); + } + this.load_from_backend = load_from_backend; + + this.load_from_json = function(json) { + var data = JSON.parse(json), + added_names, + that = this; + + clear(); + if (data == null) { + console.log('load callback: no data to load'); + return; + } + added_names = data.nodes.map(function(node) { + return that.__addNode({id:node.id, name:node.name ? node.name : node.id, + type:node.type,state:"perm", + start:new Date(node.start), + end:new Date(node.end), + status:node.status, + url:node.url, + x: node.x, + y: node.y, + }, false, false).name; + }); + data.links.forEach(function(link) { + that.addLink(link.__src, link.__dst, link.name, "perm"); + }); + this.clear_history(); + rz_bus.names.push(added_names); + } + + this.save_to_json = function() { + var d = {"nodes":[], "links":[]}; + for(var i = 0 ; i < nodes.length ; i++){ + var node = nodes[i]; + d['nodes'].push({ + "id": node.id, + "name": node.name, + "type":node.type, + "state":"perm", + "start":node.start, + "end":node.end, + "status": node.status, + "url": node.url, + "x": node.x, + "y": node.y, + }); + } + for(var j=0 ; j < links.length ; j++){ + var link = links[j]; + d['links'].push({ + "__src":link.__src.id, + "__dst":link.__dst.id, + "name":link.name + }); + } + return JSON.stringify(d); + } + + this.set_user = function(user) { + var elem = $('svg g.zoom')[0]; + this.user = user; + this.history = new history.History(this.user, this, elem); + } + + function clear_history() { + if (this.history !== undefined) { + this.history.clear(); + } + } + + this.clear_history = clear_history; + + var get_nodes = function() { + return nodes; + }; + this.nodes = get_nodes; + + var get_links = function() { return links; }; + this.links = get_links; + + function setRegularState() { + var x, node, link, s; + + for (x in nodes) { + node = nodes[x]; + s = node.state; + if (s === 'chosen' || s === 'enter' || s === 'exit') { + node.state = 'perm'; + } + } + for (x in links) { + link = links[x]; + s = link.state; + if (s === 'chosen' || s === 'enter' || s === 'exit') { + link.state = 'perm'; + } + } + } + this.setRegularState = setRegularState; + + this.findByVisitors = function(node_visitor, link_visitor) { + var n_length = nodes.length, + l_length = links.length, + selected = [], + i, + node, + link, + state; + + if (!node_visitor) { + return; + } + + for (i = 0 ; i < n_length; ++i) { + node = nodes[i]; + if (node.state == 'temp') { + continue; + } + if (node_visitor(node)) { + selected.push(node); + } + } + return selected; + } + + function markRelated(names) { + removeRelated(); + nodes.forEach(function (node) { + names.forEach(function (name) { + if (compareNames(node.name, name) && node.state != 'temp') { + node.state = 'related'; + } + }); + }); + } + this.markRelated = markRelated; + + function removeRelated() { + nodes.forEach(function (node) { + if (node.state == 'related') { + node.state = 'perm'; + } + }); + } + this.removeRelated = removeRelated; + +} + +return { + Graph: Graph, +}; + +}); diff --git a/src/client/model/util.js b/src/client/model/util.js new file mode 100644 index 00000000..d938fbc9 --- /dev/null +++ b/src/client/model/util.js @@ -0,0 +1,134 @@ +"use strict" + +/** + * model utility functions: - convert from/to client/backend data + * representations + */ +define([ 'jquery', 'model/diff' ], function($, model_diff) { + + function __sanitize_label__write(label_str){ + var ret = label_str[0].toUpperCase() + + label_str.substring(1).toLowerCase(); + return ret; + } + + function __sanitize_label__read(label_str){ + return label_str.toLowerCase(); + } + + /** + * read by adapting from backend format + */ + function adapt_format_read_node(n_raw) { + var ret; + + ret = $.extend({ + // type: + // - discard all but first label + // - adjust to lowercase + 'type' : __sanitize_label__read(n_raw['__label_set'][0]), + 'state' : 'perm', + }, n_raw); + + delete ret.__label_set; + + return ret; + } + + /** + * write by adapting to backend format + */ + function adapt_format_write_node(n_raw) { + var ret = $.extend({ + }, n_raw); + + ret['__label_set'] = [__sanitize_label__write(n_raw.type)]; + + delete ret.state; + delete ret.status + delete ret.type; + + return ret + } + + /** + * read by adapting from backend format + */ + function adapt_format_read_link_ptr(l_raw) { + var ret; + + ret = $.extend({ + '__src_id' : l_raw['__src_id'], + '__dst_id' : l_raw['__dst_id'], + // type: + // - discard all but first label + // - adjust to lowercase + '__type' : __sanitize_label__read(l_raw['__label_set'][0]), + 'state' : 'perm', + }, l_raw); + + ret['name'] = ret['__type']; + + delete ret.__label_set; + + return ret; + } + + /** + * write by adapting to backend format + */ + function adapt_format_write_link(l_raw) { + var ret = $.extend({ + '__src_id' : l_raw.source.id, + '__dst_id' : l_raw.target.id, + }, l_raw); + + ret['__label_set'] = [__sanitize_label__write(l_raw.__type)]; + + delete ret.__dst; + delete ret.__src; + delete ret.source; // introduced by d3 accessor methods + delete ret.state; + delete ret.status; + delete ret.target; + + return ret; + } + + /** + * write adapt diff from node set, link set. sets may be passed by reference + * as they are cloned + */ + function adapt_format_write_topo_diff(n_set, l_set) { + + var new_n_set = $.extend([], n_set); + var new_l_set = $.extend([], l_set); + + // filter out 'bubble' nodes + new_n_set = new_n_set.filter(function(n) { + return 'bubble' != n.type; + }); + + new_n_set = $.map(new_n_set, function(n, _) { + return adapt_format_write_node(n); + }) + + new_l_set = $.map(new_l_set, function(l, _) { + return adapt_format_write_link(l); + }) + + var topo_diff = new model_diff.new_topo_diff({ + node_set_add : new_n_set, + link_set_add : new_l_set + }); + return topo_diff; + } + + return { + adapt_format_read_node : adapt_format_read_node, + adapt_format_read_link_ptr : adapt_format_read_link_ptr, + adapt_format_write_node : adapt_format_write_node, + adapt_format_write_link : adapt_format_write_link, + adapt_format_write_topo_diff : adapt_format_write_topo_diff, + } +}); \ No newline at end of file diff --git a/src/client/robot.js b/src/client/robot.js new file mode 100644 index 00000000..700e41b6 --- /dev/null +++ b/src/client/robot.js @@ -0,0 +1,70 @@ +define(['jquery'], function($) { + +var sentence=""; +/*sentence+=" #Rhizibot is showing you a #tutorial|"; +sentence+="#Rhizi visualizes data with #Graphs and #relationships|"; +sentence+="#links and #nodes have #context and #meaning|"; + +sentence+="#entities and #concepts are #nodes|"; +sentence+="We built this entire graph in 30 seconds, try it out yourself!";*/ + +sentence+='##Rhizi is a tool for creating interactive #Networks|'; +sentence+='Create #Networks by writing #Sentences|'; +sentence+='like a #Tweet you can put a #Hashtag in your #Sentences|'; +sentence+='#Hello|'; +sentence+='#John|'; +sentence+='Put your text between #Commas to use #"Multiple words"|'; +sentence+='#"John Smith"|'; +sentence+='#"Beauty and the beast"|'; +sentence+='Use the word #And to #"Connect multiple things together"|'; +sentence+='#John likes #Apples and #Oranges and #Pistachio|'; +//sentence+='Choose the #"node type" by using the #"TAB key"|'; +sentence+='#Click on any #Node to change and modify it|'; +sentence+='#Play around and have #Fun!|'; + +var robot = function (element, sentence) { + var r = { + speed: 1, + counter: 0, + sentence: sentence, + sentencecounter: 0, + element: element || $('#textanalyser')}; + r.next_event = function () { + if(r.counter <= r.sentence.length) { + var text = r.element.val(); + r.counter++; + if (r.sentence.charAt(r.counter) !== "|") { + r.element.val(text + r.sentence.charAt(r.counter)); + if ('oninput' in document.documentElement) { + r.element.trigger('input', {}); + } + //if(Math.random()>0.9)graph.editType("x","temp",nodetypes[Math.round(Math.random()*4)]); + if (r.sentence.charAt(r.counter)==="#") { + r.sentencecounter++; + r.timeout_id = window.setTimeout( r.next_event, 30/r.speed+Math.random()*160/r.speed ); + } else { + r.timeout_id = window.setTimeout( r.next_event, 50/r.speed+Math.round(Math.random()*100/r.speed) ); + } + } else { + var e = jQuery.Event("keypress"); + e.which = 13; + e.keyCode = 13; + $("#textanalyser").trigger(e); + window.setTimeout( r.next_event, 650/r.speed ); + } + } else { + window.clearInterval(r.timeout_id); + } + } + return r; +} + +$('.logo').click(function(){ + setTimeout( robot(undefined, sentence).next_event, 1000 ); +}); + +/*var answer = confirm ("Would you like a tutorial?") +if (answer) +setTimeout( Robot, 100 );*/ + +}); // define diff --git a/src/client/rz_api_backend.js b/src/client/rz_api_backend.js new file mode 100644 index 00000000..da073f9f --- /dev/null +++ b/src/client/rz_api_backend.js @@ -0,0 +1,222 @@ +"use strict"; + +/** + * API calls designed to execute against a local backend service + */ +define(['rz_config'], function(rz_config) { + + function RZ_API_Backend() { + + var rz_server_url = 'http://' + rz_config.rz_server_host + ':' + rz_config.rz_server_port; + + /** + * issue rhizi server ajax call + */ + var ajax_rs = function(path, req_opts, on_success, on_error) { + + function on_error_wrapper(xhr, err_text, err_thrown) { + // log wrap callback + console.error('error: \'' + err_text + '\''); + if (on_error && typeof (on_error) === "function") { + on_error(err_type, err_text); + } + } + + function on_success_wrapper(xhr, text) { + // log wrap callback + var ret_data = xhr.data; + console.log('success: ' + JSON.stringify(ret_data)); + + if (on_success) { + on_success(ret_data); + } + } + + /* + * add common request options + */ + req_opts.dataType = "json"; + req_opts.contentType = "application/json; charset=utf-8"; + req_opts.error = on_error_wrapper; + req_opts.success = on_success_wrapper; + req_opts.headers = {}; + req_opts.timeout = 8000; // ms + req_opts.crossDomain = true; + + $.ajax(rz_server_url + path, req_opts); + } + + /** + * common attr_diff + */ + this.commit_diff__attr = function(attr_diff, on_success, on_error) { + + var post_dict = { + 'attr_diff' : attr_diff + } + + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/graph/diff-commit-attr', req_opts, on_success, + on_error); + } + + /** + * commit topo_diff + */ + this.commit_diff__topo = function(topo_diff, on_success, on_error) { + + var post_dict = { + 'topo_diff' : topo_diff + } + + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/graph/diff-commit-topo', req_opts, on_success, + on_error); + } + + /** + * commit vis_diff + */ + this.commit_diff__vis = function(vis_diff, on_success, on_error) { + // TODO impl + } + + /** + * commit a diff_set + */ + this.commit_diff__set = function(diff_set, on_success, on_error) { + + var post_dict = { + 'diff_set' : diff_set + } + + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/graph/diff-commit-set', req_opts, on_success, + on_error); + } + + /** + * clone rhizi repo + */ + this.clone = function(depth, on_success, on_error) { + + // prep request + var req_opts = { + type : 'POST', + }; + + ajax_rs('/graph/clone', req_opts, on_success, on_error); + } + + /** + * load node-set by id attribute + * + * @param on_complete_cb + * will be called with the returned json data on successful + * invocation + * @param on_error + * error callback + */ + this.load_node_set = function(id_set, on_success, on_error) { + + // prep request data + var post_dict = { + 'id_set' : id_set + } + + // prep request + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/load/node-set-by-id', req_opts, on_success, + on_error); + } + + /** + * load link set by src / dst id + */ + this.load_link_set = function(link_ptr_set, on_success, on_error) { + + // prep request data + var post_dict = { + 'link_ptr_set' : link_ptr_set + } + + // prep request + var req_opts = { + type : 'POST', + data : JSON.stringify(post_dict), + }; + + return ajax_rs('/load/link-set/by_link_ptr_set', req_opts, + on_success, on_error); + } + + /** + * add a node set + */ + this.add_node_set = function(n_set, on_success, on_error) { + var topo_diff = new Topo_Diff(); + topo_diff.node_set_add = n_set; + + return this.topo_diff_commit(topo_diff, on_success, on_error); + } + + /** + * add a link set + */ + this.add_link_set = function(l_set, on_success, on_error) { + var topo_diff = new Topo_Diff(); + topo_diff.link_set_add = l_set; + + return this.topo_diff_commit(topo_diff); + } + + /** + * remove node set + */ + this.remove_node_set = function() { + var topo_diff = null; + return this.topo_diff_commit(topo_diff); + } + + /** + * remove link set + */ + this.remove_link_set = function() { + var topo_diff = null; + return this.topo_diff_commit(topo_diff); + } + + /** + * update node set + */ + this.update_node_set = function(attr_diff, on_success, on_error) { + return this.attr_diff_commit(attr_diff, on_success, on_error); + } + + /** + * update link set + */ + this.update_link_set = function() { + var attr_diff = null; + return this.attr_diff_commit(null); + } + } + + return new RZ_API_Backend(); +}); diff --git a/src/client/rz_api_mesh.js b/src/client/rz_api_mesh.js new file mode 100644 index 00000000..9014dbac --- /dev/null +++ b/src/client/rz_api_mesh.js @@ -0,0 +1,22 @@ +/** + * API calls designed to execute in decentralized fashion + */ +define(['rz_api_backend'], +function(rz_api_backend) { + function RZ_API_Mesh() { + + /** + * suggest diff block and await commit/reject consensus + */ + // @ajax-trans + this.broadcast_possible_next_diff_block = function (diff_set) { + + function on_success(data){ + // TODO impl + } + + rz_api_backend.commit_diff_set(diff_set); + } + } + return new RZ_API_Mesh(); +}); diff --git a/src/client/rz_bus.js b/src/client/rz_bus.js new file mode 100644 index 00000000..38033bc4 --- /dev/null +++ b/src/client/rz_bus.js @@ -0,0 +1,15 @@ +"use strict" + +define(['consts', 'Bacon'], +function(consts, Bacon) +{ + var ui_key_bus = new Bacon.Bus(), + ui_input_bus = new Bacon.Bus(), + names_bus = new Bacon.Bus(); + + return { + ui_key: ui_key_bus, + ui_input: ui_input_bus, + names: names_bus, + }; +}); diff --git a/src/client/rz_config.js b/src/client/rz_config.js new file mode 100644 index 00000000..e55e74d0 --- /dev/null +++ b/src/client/rz_config.js @@ -0,0 +1,9 @@ +define(function() { + + return { + 'rand_id_generator' : 'hash', + 'rz_server_host': 'rhizi.local', + 'rz_server_port': '8080', + 'backend_enabled': false, + }; +}); diff --git a/src/client/rz_core.js b/src/client/rz_core.js new file mode 100644 index 00000000..7ec19692 --- /dev/null +++ b/src/client/rz_core.js @@ -0,0 +1,756 @@ +"use strict" + +define(['jquery', 'd3', 'consts', 'rz_bus', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer', 'view/selection', 'rz_config'], +function($, d3, consts, rz_bus, util, model_graph, model_core, view_helpers, view, rz_observer, selection, rz_config) { + +var addednodes = [], + vis, + graphinterval = 0, + timeline_timer = 0, + deliverables = [], + circle, // <-- should not be module globals. + scrollValue = 0, + graph, + drag, + force; + +// "CSS" for SVG elements. Reused for editing elements. +var node_text_dx = 15, + node_text_dy = '.30em', + svg_input_fo_node_x = node_text_dx, + svg_input_fo_node_y = '-.70em', + svg_input_fo_height = '30px'; + +/** + * svgInput - creates an embedded input element under a given + * + * edit_node(@sibling, @node) + * edit_link(@sibling, @link) + */ +var svgInput = (function() { + var measure_node = $('#measure-node')[0], + measure_link = $('#measure-link')[0], + original_element, + is_link; + + function appendForeignElementInputWithID(base, elemid, width, height) + { + var input = document.createElement('input'), + body = document.createElement('body'), + fo = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); + + body.appendChild(input); + + fo.setAttribute('height', height || svg_input_fo_height); + fo.style.pointerEvents = 'none'; + input.style.pointerEvents = 'all'; + fo.appendChild(body); + base.appendChild(fo); + input.setAttribute('id', elemid); + return input; + } + + function measure(text) + { + var span; + + span = is_link ? measure_link : measure_node; + span.innerHTML = text; + return span.getBoundingClientRect().width; // $().width() works too + } + + function onkeydown(e) { + var ret = undefined, + jelement = createOrGetSvgInput(), + element = jelement[0], + newname = jelement.val(), + fo = createOrGetSvgInputFO(), + d; + + if (element != this) { + console.log('unexpected editname_on_keypress this should be the svg-input element'); + } + + if (e.which == 13 || e.which == 27) { + ret = false; + d = jelement.data().d; + if (e.which == 13 && newname != d.name) { + if (d.hasOwnProperty('__src')) { + graph.editLink(d.__src.id, d.__dst.id, newname); + } else { + graph.editName(d.id, newname); + } + rz_bus.names.push([newname]); + update_view__graph(true); + } + hide(); + } + rz_bus.ui_key.push({where: consts.KEYSTROKE_WHERE_EDIT_NODE, keys: [e.which]}); + return ret; + }; + + function resize_measure(e) { + resize(measure($(e.target).val()) + 30); + } + + function resize(new_width) { + var svg_input = createOrGetSvgInput(), + fo = createOrGetSvgInputFO(); + + svg_input.css('width', new_width); + fo.attr('width', new_width); + } + + // FIXME: element being deleted. Some delete is legit - removal of related element. Some isn't (a click). + // Instead of investigating (time constraint) reparenting as sibling, and introducing + // this function. Cost of creation of element is negligble, it's just ugly.. + function createOrGetSvgInput() + { + var svg_input_name = 'svg-input', + svg_input_selector = '#' + svg_input_name, + svg_input = $(svg_input_selector); + + if (svg_input.length == 0) { + console.log('creating new svg-input'); + svg_input = $(appendForeignElementInputWithID(vis[0][0], svg_input_name)); + svg_input.on('keydown', onkeydown); + svg_input.bind('change keypress', resize_measure); + } + return svg_input; + } + + function createOrGetSvgInputFO() + { + return createOrGetSvgInput().parent().parent(); + } + + /* + * @param e visual node element + * @param n node model object + */ + function enable(e, n) { + var oldname = n.name, + svg_input = createOrGetSvgInput(), + fo = createOrGetSvgInputFO(); + + is_link = n.hasOwnProperty('__src'); + + e.parentNode.appendChild(fo[0]); // This will unparent from the old parent + if (is_link) { + fo.attr('transform', e.getAttribute('transform')); + // XXX links set the text-anchor middle attribute. no idea how to do that + fo.attr('x', -$(e).width() / 2); + fo.attr('y', -$(e).height() / 2 - 3); // XXX This minus 3 is only kinda ok. + fo.attr('class', 'svg-input-fo-link'); + } else { + fo.attr('x', svg_input_fo_node_x); + fo.attr('y', svg_input_fo_node_y); + fo.attr('transform', null); + fo.attr('class', 'svg-input-fo-node'); + } + // Set width correctly + resize(measure(oldname) + 30); + fo.show(); + svg_input.val(oldname); + svg_input.data().d = n; + svg_input.focus(); + if (original_element) { + original_element.show(); + } + original_element = $(e); + original_element.hide(); + // TODO: set cursor to correct location in text + } + + function hide() { + createOrGetSvgInputFO().hide(); + if (original_element && original_element.show) { + original_element.show(); + } + } + + return { + enable: enable, + hide: hide, + }; +}()); + + + +function recenterZoom() { + vis.attr("transform", "translate(0,0)scale(1)"); +} + +// zoom or drag +var zoomInProgress = false; + +var initDrawingArea = function () { + + function zoom() { + zoomInProgress = true; + vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); + d3.event.sourceEvent.stopPropagation(); + } + + function dragstarted(d) { + d3.event.sourceEvent.stopPropagation(); + d3.select(this).classed("dragging", true); + d.dragstart = {clientX:d3.event.sourceEvent.clientX, clientY:d3.event.sourceEvent.clientY}; + force.stop(); + } + + function dragged(d) { + d.x = d3.event.x; + d.y = d3.event.y; + tick(); + } + + function dragended(d) { + d3.select(this).classed("dragging", false); + d3.select(this).classed("fixed", true); // TODO: this is broken since we override all the classes. Need to switch to class addition/removal (i.e. use classed for everything) or set class in one location (so here just set a value on the node, not the element) + if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 || + d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) { + tick(); + force.resume(); + } + } + + graph = new model_graph.Graph(); + + var user_id = $('#user_id'), + user = user_id.text(); + + if (user_id.length > 0) { + console.log('found user ID: \'' + user + '\''); + graph.set_user(user); + } + + var el = document.body; + vis = d3.select(el).append("svg:svg") + .attr('id', 'canvas_d3') + .attr("width", '100%') + .attr("height", '100%') + .attr("pointer-events", "all") + .append("g") + .attr("class", "zoom"); + + d3.select(el).select("svg").append("svg:defs") + .data(["end"]) // Different link/path types can be defined here + .append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 22) + .attr("refY", -1.5) + .attr("markerWidth", 4) + .attr("markerHeight", 4) + .attr("orient", "auto") + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + /* + * init zoom behavior + */ + var zoom_obj = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); + zoom_obj(d3.select('#canvas_d3')) + d3.select("svg").on("dblclick.zoom", null); // disable zoom on double click + + $('svg').click(svg_click_handler); + + // SVG rendering order is last rendered on top, so to make sure + // all links are below the nodes we group them under a single g + vis.append("g").attr("id", "link-group"); + vis.append("g").attr("id", "selected-link-group"); + + drag = d3.behavior.drag() + .origin(function(d) { return d; }) + .on("dragstart", dragstarted) + .on("drag", dragged) + .on("dragend", dragended); + + // $('#canvas_d3').dblclick(canvas_handler_dblclick); - see #138 + if (rz_config.backend_enabled){ + graph.load_from_backend( function(){ + update_view__graph(false); + }); + } +} + +function init_force_layout(){ + var el = document.body; + var w = $(el).innerWidth(), + h = $(el).innerHeight(); + + force = d3.layout.force() + .distance(120) + .gravity(0.12) + .charge(-1800) + .size([w, h]) + .on("tick", tick) + .start(); +} + +initDrawingArea(); +init_force_layout(); + +/** + * find the visual element counterpart of a given model object. This relies on + * the visual element having an id attribute equal to the object's id. + * + * @return null if visual element is not found + */ +function locate_visual_element(model_obj){ + var id_sel = $('#' + model_obj.id); + if (0 == id_sel.length){ + console.warn('unable to find visual element for model object: object id: ' + model_obj.id.toString()) + return null; + } + return id_sel[0]; +} + +/** + * add node on canvas double click + */ +function canvas_handler_dblclick(){ + var n = model_core.create_node__set_random_id(); + n.name = ''; // will be set by user + + graph.addNode(n); + update_view__graph(); + + var n_ve = locate_visual_element(n); // locate visual element + + var on_slowdown_cb = function(){ + svgInput.enable($(n_ve).find('.nodetext'), n); + observer.disconnect(); + } + var mutation_handler = rz_observer.new_Mutation_Handler__on_dxy_slowdown(on_slowdown_cb); + var observer = rz_observer.new_MutationObserver(mutation_handler); + mutation_handler.on_slowdown_threshold_reached; + + observer.observe(n_ve, { + subtree: false, + childList : false, + attributes: true, + attributeOldValue : true, + }); +} + +/** + * update view: graph + */ +function update_view__graph(no_relayout) { + var node, + link, + link_g, + linktext, + nodetext, + unselected_link_group = document.querySelector('#link-group'), + selected_link_group = document.querySelector('#selected-link-group'); + + link = vis.selectAll("g.link") + .data(graph.links(), function(d) { return d.id; }); + + link_g = link.enter().append('g') + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr('class', 'link graph') + + link_g.append("path") + .attr("class", function(d) { + return d.state + ' link graph'; + }) + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr("marker-end", "url(#end)"); + + // second path for larger click area + link_g.append("path") + .attr("class", "ghostlink") + .on("click", function(d, i) { + if (zoomInProgress) { + // don't disable zoomInProgress, it will be disabled by the svg_click_handler + // after this events bubbles to the svg element + return; + } + var that = this, + src = this.link.__src, + dst = this.link.__dst; + + view.edge_info.on_delete(function () { + graph.removeLink(that.link); + update_view__graph(true); + view.edge_info.hide(); + }); + view.edge_info.show(d); + selection.update([src, dst]); + update_view__graph(true); + }); + + link.attr("class", function(d, i){ + var temp_and = (d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp") ? "temp_and" : ""; + + return ["graph link", temp_and, selection.selected_class(d)].join(' '); + }); + + link.selectAll('path.link') + .attr('class', function(d) { + return [d.state, selection.selected_class(d), "link graph"].join(' '); + }); + + link.exit().remove(); + + vis.selectAll('.ghostlink') + .data(graph.links()) + .each(function (d) { + this.link = d; + }); + + linktext = vis.selectAll(".linklabel") + .data(graph.links(), function(d) { return d.id; }); + linktext.enter() + .append("text") + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr("class", function(d) { + return ["linklabel graph", selection.selected_class(d)].join(' '); + }) + .attr("text-anchor", "middle") + .on("click", function(d, i) { + if (d.state !== "temp") { + svgInput.enable(this, d); + } + }); + + linktext + .text(function(d) { + var name = d.name || ""; + if (!(d.__dst.state === "temp" || + d.__src.state === "chosen" || d.__dst.state === "chosen")) { + return ""; + } + if (name.length < 25 || d.__src.state === "chosen" || + d.__dst.state === "chosen" || d.state==="temp") { + return name; + } else { + return name.substring(0, 14) + "..."; + } + }); + + linktext.exit().remove(); + + node = vis.selectAll(".node") + .data(graph.nodes(), function(d) { + return d.id; + }); + + var nodeEnter = node.enter() + .append("g") + .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping + .attr('visibility', 'hidden') // made visible on first tick + .call(drag); + + // reorder nodes so selected are last, and so rendered last, and so on top. + (function () { + var ontop = [], + bubble; + + node.each(function (d) { + this.node = d; + }) + .attr('class', function(d) { + if (selection.node_selected(d)) { + if (d.type == 'bubble') { + bubble = this; + } else { + ontop.push(this); + } + } + return ['node', selection.selected_class(d)].join(' '); + }); + if (bubble === undefined) { + // nothing to do if there is no bubble + return; + } + function reparent(new_parent, element) { + if (element.parentNode == new_parent) { + return; + } + new_parent.appendChild(element); + } + // move link to correct group + // O(|links|*|ontop|) + link.each(function (d) { + if (ontop.some(function (node) { + var d_node = node.node; + return d.__src == d_node || d.__dst == d_node; + })) + { + reparent(selected_link_group, this); + } else { + reparent(unselected_link_group, this); + } + }); + linktext.each(function (d) { + if (selection.node_selected(d)) { + ontop.push(this); + } + }); + function moveToEnd(e) { + e.parentNode.appendChild(e); + } + moveToEnd(bubble); + ontop.reverse().forEach(function (e) { + moveToEnd(e); + }); + var count_links = function() { + return selected_link_group.childElementCount + unselected_link_group.childElementCount; + }; + // put back on top link group on top + selected_link_group.parentNode.insertBefore(selected_link_group, bubble.nextSibling); + })(); + + nodetext = nodeEnter.insert("text") + .attr("class", "nodetext graph") + .attr("dx", node_text_dx) + .attr("dy", node_text_dy) + .on("click", function(d, i) { + if (d3.event.defaultPrevented) { + // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on + return; + } + if (d.state !== "temp") { + svgInput.enable(this, d); + selection.update([d]); + showNodeInfo(this.parentNode.node, i); + } + d3.event.stopPropagation(); + }); + + node.select('g.node text') + .text(function(d) { + if (!d.name) { + return d.type == 'bubble' ? "" : "_"; + } + if (d.state === "temp" || d.state === 'chosen' + || d.state === "enter" || d.state === "exit") { + return d.name; + } else { + if (d.name.length < 28) { + return d.name; + } else { + return d.name.substring(0, 25) + "..."; + } + } + }); + + circle = nodeEnter.insert("circle"); + node.select('g.node circle') + .attr("class", function(d) { + return d.type + " " + d.state + " circle graph"; + }) + .attr("r", function(d) { + return view_helpers.customSize(d.type) - 2; + }) + .on("click", function(d, i) { + if (d3.event.defaultPrevented) { + // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on + return; + } + d3.event.stopPropagation(); + selection.update([d]); + if(d.state !== "temp") { + showNodeInfo(d, i); + } + }); + circle.append("svg:image") + .attr("class", "status graph") + .attr('x', -7) + .attr('y', -8) + .attr('width', 15) + .attr('height', 15) + .attr("xlink:href", function(d) { + switch (d.status) { + case "done": + return "res/img/check.png"; + break; + case "current": + return "res/img/wait.png"; + break; + case "waiting": + return "res/img/cross.png"; + break; + } + }); + + node.exit().remove(); + + //update deliverables + deliverables = []; + var nodes = graph.nodes(); + for (var i = 0; i < nodes.length; i++) { + var current = nodes[i]; + if (current.type === "third-internship-proposal") { + deliverables.push({ + "id": nodes[i].id, + "startdate": nodes[i].start, + "enddate": nodes[i].end + }); + } + //Do something + } + + force.nodes(graph.nodes()) + .links(graph.links()) + + if (no_relayout) { + // XXX If we are stopped we need to update the text of the links at least, + // and this is the simplest way + tick(); + } else { + force.alpha(0.1).start(); + } +} + + + +var debug_print = function(message) { + var element = $(".debug"); + if (element.length == 1) { + element.html(message); + } else { + console.log(message); + } +} + +function check_for_nan(x) { + if (Number.isNaN(x)) { + console.log('nan problem'); + force.stop(); + } + return Number.isNaN(x); +} + +var newnodes=1; +function tick(e) { + //console.log(e); + //$(".debug").html(force.alpha()); + var node = vis.selectAll(".node") + .data(force.nodes(), function(d) { + return d.id; + }); + var link = vis.selectAll("path.link") + .data(graph.links(), function(d) { + return d.id; + }); + var linktext = vis.selectAll(".linklabel").data(graph.links()); + + function transform(d) { + if (check_for_nan(d.x) || check_for_nan(d.y)) { + return; + } + return "translate(" + d.x + "," + d.y + ")"; + } + + //circles animation + var tempcounter = 0, + temptotal = graph.nodes().filter(function(d){ + return d.state === "temp" && d.type !== "chainlink" && d.type !== "bubble"; + }).length; + if (temptotal !== newnodes) { + newnodes += temptotal / 15 / (newnodes * newnodes); + } + newnodes = Math.max(1, Math.min(newnodes, temptotal)); + graph.nodes().forEach(function(d, i) { + var r, a; + if (d.state === "temp") { + tempcounter++; + if (d.type==="chainlink" || d.type==="bubble") { + d.x = window.innerWidth / 2; + d.y = window.innerHeight / 2; + } else { + r = 60 + newnodes * 20; + a = -Math.PI + Math.PI * 2 * (tempcounter-1) / newnodes + 0.3; + d.x = window.innerWidth / 2 + r * Math.cos(a); + d.y = window.innerHeight / 2 + r * Math.sin(a); + } + check_for_nan(d.x); + check_for_nan(d.y); + } + }); + + link.attr("d", function(d, i) { + var d_val, + ghost; + + var dx = d.__dst.x - d.__src.x, + dy = d.__dst.y - d.__src.y, + dr = Math.sqrt(dx * dx + dy * dy); + d_val = "M" + d.__src.x + "," + d.__src.y + "L" + d.__dst.x + "," + d.__dst.y; + // update ghostlink position + ghost = $(this.nextElementSibling); + ghost.attr("d", d_val); + return d_val; + }); + + linktext.attr("transform", function(d) { + return "translate(" + (d.__src.x + d.__dst.x) / 2 + "," + (d.__src.y + d.__dst.y) / 2 + ")"; + }); + + node.attr("transform", transform); + + // After initial placement we can make the nodes visible. + //links.attr('visibility', 'visible'); + node.attr('visibility', 'visible'); +} + +function showNodeInfo(d, i) { + view.node_info.on_save(function(e, form_data) { + + graph.update_node(d, form_data, function(){ + var old_type = d.type, + new_type = form_data.type; + + if (new_type != old_type) { + view.node_info.show(d); + } + + view.node_info.hide(); + update_view__graph(true); + }); + + return false; + }); + + view.node_info.on_delete(function() { + if (confirm('This node and all its connections will be deleted, are you sure?')) { + graph.removeNode(d.id); + update_view__graph(false); + view.node_info.hide(); + } + }); + + view.node_info.show(d); +} + +function svg_click_handler(e) { + if (zoomInProgress) { + zoomInProgress = false; + return; + } + if (e.originalEvent.target.nodeName != 'svg') { + return; + } + svgInput.hide(); + selection.clear(); + view.hide(); + update_view__graph(true); +} + +return { + graph: graph, + force: force, + load_from_json: function(result) { + graph.load_from_json(result); + recenterZoom(); + update_view__graph(false); + }, + update_view__graph : update_view__graph, +} +}); /* close define call */ diff --git a/src/client/rz_observer.js b/src/client/rz_observer.js new file mode 100644 index 00000000..718fd0a2 --- /dev/null +++ b/src/client/rz_observer.js @@ -0,0 +1,114 @@ +"use strict" + +/** + * DOM object observers + */ +define( +[], +function() { + +var MutationObserver = window.MutationObserver; +// more portable version: var MutationObserver = +// window.MutationObserver || +// window.WebKitMutationObserver; + +/** + * observe SVG object's translate attribute (x,y values), measure + * change rate & invoke on_slowdown_threshold_reached() upon + * reaching change slowdown threshold. + * + * Caller is responsible to disconnect observer. + */ +function Mutation_Handler__on_dxy_slowdown( + on_slowdown_threshold_reached) { + + var x_cur, y_cur, d, dx, dy, avg_d = 0; + + this.on_slowdown_threshold_reached = on_slowdown_threshold_reached; + + /** + * handle observer mutation + */ + this.handle_mutation = function(m) { + if ('transform' != m.attributeName || null == m.oldValue) { + return; + } + + /* + * parsed txt example: + * "translate(173.6007550157428,275.43228723527193)" + */ + var rgx_m = m.oldValue.match(/\((\d+.\d+),(\d+.\d+)\)/); + x_cur = rgx_m[1]; + y_cur = rgx_m[2]; + if (undefined == this.x_prv) { + this.x_prv = x_cur; + this.y_prv = y_cur; + return; + } + dx = x_cur - this.x_prv; + dy = y_cur - this.y_prv; + + var d = Math.sqrt(dx * dx + dy * dy); + // average across last samples + avg_d = (1.0 - this.most_recent_sample_weight) * avg_d + + this.most_recent_sample_weight * d; + + // debug + // console.log({ + // 'dx' : dx, + // 'dy' : dy, + // 'd' : d, + // 'avg_d' : avg_d, + // }); + + if (avg_d < this.slowdown_threshold) { + // slowdown threshold reached + if (this.on_slowdown_threshold_reached) { + this.on_slowdown_threshold_reached(); + } + } + + this.x_prv = x_cur; + this.y_prv = y_cur; + } +} + +function new_Mutation_Handler__on_dxy_slowdown( + on_slowdown_threshold_reached, slowdown_threshold, + most_recent_sample_weight) { + slowdown_threshold = slowdown_threshold || 0.07; + most_recent_sample_weight = most_recent_sample_weight || 0.3; + var ret = new Mutation_Handler__on_dxy_slowdown( + on_slowdown_threshold_reached); + ret.slowdown_threshold = slowdown_threshold; + ret.most_recent_sample_weight = most_recent_sample_weight; + return ret; +} + +function new_MutationObserver(handler) { + /* + * FIXME implemente with inheritence - currently triggers + * 'illegal invocation', possibly due to some interaction with + * requirejs + */ + // var o = new + // window.MutationObserver(Mutation_Observer.handler); + // var ret = Object.create(o); + // ret. = undefined; + // return ret; + var for_each_mutaion = function(m_set) { + m_set.forEach(function(m) { + handler.handle_mutation(m); + }) + }; + + return new MutationObserver(for_each_mutaion); +} + +return { + new_MutationObserver : new_MutationObserver, + new_Mutation_Handler__on_dxy_slowdown : new_Mutation_Handler__on_dxy_slowdown, +}; + +}); diff --git a/src/client/textanalysis.js b/src/client/textanalysis.js new file mode 100644 index 00000000..830c16c1 --- /dev/null +++ b/src/client/textanalysis.js @@ -0,0 +1,501 @@ +"use strict"; + +define(['rz_core', 'model/core', 'model/util', 'model/diff', 'rz_bus', 'consts'], +function(rz_core, model_core, model_util, model_diff, rz_bus, consts) { + +var typeindex = 0; +var nodetypes = consts.nodetypes; +var typeStack = []; + +var lastnode; + +var sugg = {}, // suggestions for autocompletion of node names + suggestions_options = new Bacon.Bus(); // TODO: Property: same as bus, but with initial value + +var ANALYSIS_NODE_START = 'ANALYSIS_NODE_START'; +var ANALYSIS_NODE = 'ANALYSIS_NODE' +var ANALYSIS_LINK = 'ANALYSIS_LINK'; + +function selectedType() +{ + return nodetypes[typeindex]; +} + +function autoSuggestAddName(name) +{ + /* note that name can contain spaces - this is ok. We might want to limit this though? */ + sugg[name] = 1; + suggestions_options.push(sugg); +} + +/* up_to_two_renames: + * + * allow one letter or 'new node' to anything changes */ +function up_to_two_renames(graph, old_name, new_name) +{ + var not_one_letter = false; + var k; + /* Allowed renames: + * no change + * s1 is substring of s2 + * older (s1) node being 'new node' + */ + function allowed_rename(s1, s2) + { + return (s1 == s2 || + s1 == 'new node' || + s1.substr(0, s2.length) == s2 || + s2.substr(0, s1.length) == s1); + } + + if (old_name.length != new_name.length) { + console.log('bug: up_to_two_renames: not equal inputs'); + return; + } + if (old_name.length > 2) { + console.log('bug: up_to_two_renames: input length 2 < ' + old_name.length); + return; + } + if (old_name.length == 2) { + if (allowed_rename(old_name[0], new_name[1]) && + allowed_rename(old_name[1], new_name[0])) { + old_name = [old_name[1], old_name[0]]; + } else { + if (!allowed_rename(old_name[0], new_name[0]) || + !allowed_rename(old_name[1], new_name[1])) { + not_one_letter = true; + } + } + } + if (not_one_letter) { + console.log('bug: up_to_two_renames: not one letter changes'); + console.log(old_name); + console.log(new_name); + return; + } + for (k = 0 ; k < old_name.length ; ++k) { + graph.editNameByName(old_name[k], new_name[k]); + } +} + +// TODO: add escape char, i.e. r"bla\"bla" -> ['bla"bla'] +function tokenize(text, node_token, quote) +{ + var segment = [], + subsegment = [], + sentence = [], + quoteword; + var j; + + segment = text.split(node_token); + for (j = 0; j < segment.length; j++) { + if (j !== 0) sentence.push(node_token); + subsegment = segment[j].split(" "); + if (subsegment.length === 0) { + sentence.push(" "); + } + for (var k = 0; k < subsegment.length; k++) { + if (subsegment[k] !== " " && subsegment[k] !== "") { + if (subsegment[k].charAt(0) === quote) { + quoteword = ""; + do { + quoteword += subsegment[k] + ' '; + if(subsegment[k].charAt(subsegment[k].length-1) !== quote)k++; + } while (k < subsegment.length && subsegment[k].charAt(subsegment[k].length - 1) !== quote); + if (subsegment[k] && subsegment[k]!==quoteword.replace(/ /g, "")) { + quoteword += subsegment[k]; + } + sentence.push(quoteword.replace(new RegExp(quote, 'g'), "")); + } else { + sentence.push(subsegment[k]); + } + } + } + } + return sentence; +} + +/* + * textAnalyser + * + * Input: + * @newtext - new sentence + * @finalize - is this an intermediate editing state or are we editing the graph + * + * Output: + * none + * + * Side effect: + * updating graph (global) + * + * Implementation notes: + * There is no well defined grammer. The translation goes from obvious to not + * so much for more complex sentences involving more than two nodes (two '#' + * marks). + * + */ +var textAnalyser = function (newtext, finalize) { + var sentence, + token_set_new_node_names = [], // token set representing new node names + token_set_new_link_names = [], // token set representing new link names + linkindex = 0, + nodeindex = 0, + orderStack = [], + and_count = 0, + prefix = "", + m, + word, + completeSentence, + typesetter, starGraph, + n, + link_hash = {}, + yell_bug = false, // TODO: fix both issues + NODE = "NODE", + LINK = "LINK", + START = "START", + ret = model_diff.new_topo_diff(); + + function addNode(name, type, state) { + if (type === undefined) { + console.log('bug: textanalyser.addNode of type undefined'); + } + var node = model_core.create_node_from_spec( + {'name':name, + 'type':type, + 'state':state}); + + ret.node_set_add.push(node); + } + + function __addLink(src, dst, name, state) { + if (!src || !dst) { + if (yell_bug) { + console.log('bug - adding link (' + src + ', ' + dst + ')'); + } + return; + } + if (link_hash[src] && link_hash[src][dst]) { + if (yell_bug) { + console.log('bug - adding link twice (' + src + ', ' + dst + ')'); + } + return; + } + if (!link_hash[src]) { + link_hash[src] = {}; + } + link_hash[src][dst] = 1; + + var link = {'__src': src, '__dst':dst, 'name':name, 'state':state}; // can't use model_core.create_link_from_spec as src,dst are only names + ret.link_set_add.push(link); + } + + function apply_conjugator_link_logic(link, drop_conjugator_links){ + if (drop_conjugator_links && link.name && (link.name.replace(/ /g,"") === "and")) { + link.state = "temp"; + } + } + + if (newtext.indexOf('#') == -1 || finalize) { + lastnode = null; + } + + //Sentence Sequencing + //Build the words and cuts the main elements + sentence = tokenize(newtext, '#', '"'); + + // build new node,link arrays in order of appearance + for (m = 0; m < sentence.length; m++) { + switch (sentence[m]) { + case "#": + orderStack.push(START); + break; + case "and": + case "+": + case ",": + case "&": + sentence[m] = "and"; + and_count++; + //orderStack.push("AND"); + default: + if (orderStack[orderStack.length - 1] === START) { + orderStack.push(NODE); + token_set_new_node_names.push(sentence[m]); + linkindex++; + } else if (orderStack[orderStack.length - 1] === NODE) { + orderStack.push(LINK); + if (!token_set_new_link_names[linkindex]) { + token_set_new_link_names[linkindex] = sentence[m] + " "; + } else { + token_set_new_link_names[linkindex] += sentence[m] + " "; + } + } else { + if (!token_set_new_link_names[linkindex]) { + token_set_new_link_names[linkindex] = sentence[m] + " "; + } else { + token_set_new_link_names[linkindex] += sentence[m] + " "; + } + } + if (token_set_new_node_names.length === 0) { + prefix += (prefix.length > 0 ? ' ' : '') + sentence[m]; + } + break; + } + } + + starGraph = (token_set_new_link_names.length - and_count) >= 3 || + ((token_set_new_link_names.length - and_count >= 1) && + token_set_new_link_names.length > 2 && + orderStack.length > 1 && + orderStack[orderStack.length - 1] != NODE); + + //PREFIX not null case - put complete sentence in first link. + if (prefix && !starGraph) { + token_set_new_link_names[1] = prefix + " " + token_set_new_node_names[0] + + (token_set_new_link_names[1] !== undefined || token_set_new_node_names[1] !== undefined ? + " " : "") + + (token_set_new_link_names[1] !== undefined ? token_set_new_link_names[1] : "") + + (token_set_new_node_names[1] !== undefined ? token_set_new_node_names[1] : ""); + } + + //WRITE COMPLETE SENTENCE + linkindex = 0; + nodeindex = 0; + word = ""; + completeSentence = prefix.length > 0 ? String(prefix) + " " : ""; + for (m = 0; m < orderStack.length; m++) { + if (orderStack[m] === NODE) { + word += " (" + token_set_new_node_names[nodeindex] + ") "; + completeSentence += token_set_new_node_names[nodeindex] + " "; + nodeindex++; + } else if (orderStack[m] === LINK) { + word += " -->" + token_set_new_link_names[nodeindex] + " --> "; + completeSentence += token_set_new_link_names[nodeindex]; + } + } + completeSentence = completeSentence.trim(); + + //REBUILD GRAPH + linkindex = 0; + nodeindex = 0; + + //CHANGE TO PERMANENT STATE AND UPDATE SUGGESTIONLIST + typesetter = ""; + if (finalize === true) { + typesetter = "perm"; + for (n = 0; n < token_set_new_node_names.length; n++) { + autoSuggestAddName(token_set_new_node_names[n]); + } + } else { + typesetter = "temp"; + } + + //ADD SURROUNDING BUBBLE + if (orderStack.length > 0) { + addNode("", "bubble","temp"); + } + + //0-N ORDER STACK + for (m = 0; m < orderStack.length - 1; m++) { + switch (orderStack[m]) { + case START: + if (!typeStack[nodeindex]) { + typeStack[nodeindex] = selectedType(); + } + break; + case NODE: + addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter); + if (!starGraph && nodeindex > 0) { + __addLink(token_set_new_node_names[nodeindex - 1], + token_set_new_node_names[nodeindex], + token_set_new_link_names[linkindex], typesetter); + } + nodeindex++; + break; + case LINK: + linkindex++; + break; + } + } + + //FINAL N ORDER + switch (orderStack[orderStack.length - 1]) { + case START: + typeStack[nodeindex] = selectedType(); + addNode("new node", typeStack[nodeindex], "temp"); + if (!starGraph && nodeindex > 0) { + __addLink(token_set_new_node_names[nodeindex - 1], "new node", + token_set_new_link_names[linkindex], "temp"); + and_connect("new node"); + } + ret.state = ANALYSIS_NODE_START; + break; + case NODE: + typeStack[nodeindex] = selectedType(); + addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter); + if (!starGraph && nodeindex > 0) { + __addLink(token_set_new_node_names[nodeindex - 1], + token_set_new_node_names[nodeindex], + token_set_new_link_names[linkindex], typesetter); + and_connect(token_set_new_node_names[nodeindex]); + } + ret.state = ANALYSIS_NODE_START; + break; + case LINK: + linkindex++; + addNode("new node", selectedType(), "temp"); + if (!starGraph) { + __addLink(token_set_new_node_names[nodeindex - 1], "new node", token_set_new_link_names[linkindex], "temp"); + and_connect("new node"); + } + ret.state = ANALYSIS_LINK; + break; + } + + //EXTERNAL AND CONNECTION CHECKING + function and_connect(node) { + var verb; + for(var x=0;x 500) { + element.css('width', text.length * 8 + 20); + } + // text changed + text = element.val(); + analyzeSentence(text, false); + suggestionChange = false; + } + }, 50); + } + } +}; +}); // define diff --git a/src/client/util.js b/src/client/util.js new file mode 100644 index 00000000..3fd4aecb --- /dev/null +++ b/src/client/util.js @@ -0,0 +1,72 @@ +"use strict" + +define(function() { + + function assert(condition, message) { + if (false == condition) { + message = message || "Assertion failed"; + if (typeof Error !== "undefined") { + throw new Error(message); + } + throw message; // Fallback + } + } + + function set_from_array(a) { + var ret = {}; + for (var k = 0; k < a.length; ++k) { + ret[a[k]] = 1; + } + return ret; + } + + function set_from_object(o) { + var ret = {} + for ( var k in o) { + ret[k] = 1; + } + return ret; + } + + function set_diff(sa, sb) { + var ret = { + a_b : [], + b_a : [] + }; + var i; + for (i in sa) { + if (!(i in sb)) { + ret.a_b.push(i); + } + } + for (i in sb) { + if (!(i in sa)) { + ret.b_a.push(i); + } + } + return ret; + } + + function array_diff(aa, ab) { + var sa = set_from_array(aa); + var sb = set_from_array(ab); + return set_diff(sa, sb); + } + + // TODO: jquery BBQ: $.deparam.querystring().json; + function getParameterByName(name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); + } + + return { + assert: assert, + set_from_array: set_from_array, + set_from_object: set_from_object, + set_diff: set_diff, + array_diff: array_diff, + getParameterByName: getParameterByName, + }; +}); diff --git a/src/client/view/completer.js b/src/client/view/completer.js new file mode 100644 index 00000000..d2593321 --- /dev/null +++ b/src/client/view/completer.js @@ -0,0 +1,229 @@ +define( +['jquery', 'Bacon'], +function($, Bacon) { + +function unquoted(name) +{ + var start = 0, + end = name.length; + + if (name.length >= 1) { + if (name.charAt(0) == '"') { + start = 1; + if (name.length > 1 && name.charAt(name.length - 1) == '"') { + end = name.length - 1; + } + } + return name.substring(start, end); + } + return name; +} + +function setCaret(e, num) +{ + e.selectionStart = e.selectionEnd = num; +} + +var completer = (function (input_element, dropdown, base_config) { + var config = get_config(base_config), + dropdown_raw = dropdown[0], + options_bus = new Bacon.Bus(), + options = [], + selected_index = -1, + input_element_raw = input_element[0], + completion_start = 0, + completion_end = 0, + minimum_length = 1; + + // turn off the browser's autocomplete + input_element.attr('autocomplete', 'off'); + + //$('.ui-autocomplete').css('width', '10px'); + options_bus.onValue(function update_options(new_options) { + options = new_options; + }); + + input_element.keyup(function(e) { + var ret = undefined; + switch (e.keyCode) { + case 38: //UP + prev_option(); + ret = false; + break; + case 40: //DOWN + next_option(); + ret = false; + break; + case 27: // Escape + hide(); + ret = false; + break; + default: + // This catches cursor move due to keyboard events. no event for cursor movement itself + // below we catch cursor moves due to mouse click + oninput(input_element_raw.value, input_element_raw.selectionStart); + } + return ret; + }); + input_element.keydown(function(e) { + switch (e.keyCode) { + case 38: + case 40: + return false; + case 9: // Tab + if (config.hideOnTab) { + hide(); + } + break; + } + }); + + function get_config(base) { + return { + triggerStart: base && base.triggerStart || '#', + triggerEnd: base && base.triggerEnd || ' ', + hideOnTab: base && base.hasOwnProperty('hideOnTab') ? base.hideOnTab : true, + }; + } + + function completions(text) + { + var ret = [], + noquotes = unquoted(text.toLowerCase()); + + for (var name in options) { + if (name.toLowerCase().indexOf(noquotes) === 0) { + ret.push(name); + } + } + return ret; + } + + function show() { + if (dropdown.children().length > 0) { + dropdown.show(); + } + } + function hide() + { + dropdown.hide(); + } + + /*** + * #this is a # + * ^ + * + * #this is a #t + * ^ + * + * #this and #that then #he + * ^ + */ + function oninput(text, cursor) { + var hash = text.slice(0, cursor).lastIndexOf(config.triggerStart); + // TODO check if current completion has been invalidated + _invalidateSelection(); + hide(); + dropdown_raw.innerHTML = ""; // remove all elements + if (hash == -1 && config.triggerStart != ' ') { // space matches start of string too + return; + } + var space = text.slice(hash + 1).indexOf(config.triggerEnd); + space = space == -1 ? text.length : space; + if (space < cursor) { + return; + } + completion_start = hash + 1; + completion_end = space; + var string = text.slice(completion_start, completion_end); + if (string.length < minimum_length) { + return; + } + completions(string).forEach(function(name) { + var suggestion = $('
' + name + '
'); + suggestion.on('click', function(e) { + _applySuggestion(name); + input_element.focus(); + }); + dropdown.append(suggestion); + }); + show(); + } + + function _invalidateSelection() { + update_highlighting(-1); + } + + function _move_option(change, default_value) { + var next, + n = dropdown.children().length; + + if (n == 0) { + return; + } + show(); + if (selected_index == -1) { + next = default_value; + } else { + next = (selected_index + change) % n; + } + update_highlighting(next); + } + function next_option() { + _move_option(1, 0); + } + function prev_option() { + _move_option(dropdown.children().length - 1, dropdown.children().length - 1); + } + function _get_option(index) { + if (dropdown.children().length <= index) { + console.log('error: dropdown does not contain index ' + index + + ', it has ' + dropdown.children().length + ' elements'); + return ''; + } + var e = dropdown.children()[index], + s = e.innerText || e.textContent; + if (s.indexOf(' ') != -1) { + return '"' + s + '"'; + } + return s; + } + function _choice(i) { + return dropdown.children().eq(i); + } + function update_highlighting(new_index) { + if (selected_index != -1) { + _choice(selected_index).removeClass('selected'); + } + if (new_index != -1) { + _choice(new_index).addClass('selected'); + } + selected_index = new_index; + } + function _applySuggestion(str) { + var cur = input_element.val(), + start = cur.slice(0, completion_start) + str + ' '; + input_element.val(start + cur.slice(completion_end)); + setCaret(input_element, start.length); + oninput('', 0); + } + function handleEnter() { + if (selected_index == -1) { + return false; + } + _applySuggestion(_get_option(selected_index)); + return true; + } + + return { + options: options_bus, + oninput: oninput, + next_option: next_option, + prev_option: prev_option, + handleEnter: handleEnter, + }; +}); + +return completer; + +}); diff --git a/src/client/view/edge_info.js b/src/client/view/edge_info.js new file mode 100644 index 00000000..d711e200 --- /dev/null +++ b/src/client/view/edge_info.js @@ -0,0 +1,36 @@ +"use strict" + +define(['view/internal'], +function(internal) { + +var delete_button = internal.edit_tab.get('edge', '#deleteedge'), + delete_callback = undefined; + +delete_button.on('click', function() { + if (delete_callback) { + delete_callback(); + } +}); + +function show(link) +{ + internal.edit_tab.show('edge'); + internal.edit_tab.get('edge', '#edgetitle').html(link.name); +} + +function hide() +{ + internal.edit_tab.hide(); +} + +function on_delete(f) +{ + delete_callback = f; +} + +return { + show: show, + hide: hide, + on_delete: on_delete, +}; +}); diff --git a/src/client/view/helpers.js b/src/client/view/helpers.js new file mode 100644 index 00000000..ad11763a --- /dev/null +++ b/src/client/view/helpers.js @@ -0,0 +1,87 @@ +"use strict" + +define(function() { +function customColor(type) { + var color; + switch (type) { + case "person": + color = '#FCB924'; //blue + break; + case "club": + color = '#ee3654'; //magenta + break; + case "skill": + color = '#fad900'; //yellow + break; + case "third-internship-proposal": + color = '#33c2e0'; //cyan + break; + case "internship": + color = '#ff8b11'; //orange + break; + case "interest": + color = '#8b3ab0'; //purple + break; + case "project": + color = "#40C200"; //green + break; + case "empty": + color = "#919095"; //mid-grey + break; + case "chainlink": + color = "#363636"; //dark-grey + break; + case "bubble": + color = "rgba(255,255,255,0.2)"; // white, 0.2 opaque + break; + default: + console.log('bug: unknown type ' + type); + color = '#d4d4d9'; //mid-light grey + break; + } + return color; +} + +function customSize(type) { + var size; + switch (type) { + case "person": + size = 12; + break; + case "club": + size = 12; + break; + case "skill": + size = 12; + break; + case "third-internship-proposal": + size = 12; + break; + case "internship": + size = 12; + break; + case "interest": + size = 12; + break; + case "empty": + size = 9; + break; + case "chainlink": + size = 8; + break; + case "bubble": + size = 180; + break; + default: + size = 9; + break; + } + return size; +} + + +return { + customSize: customSize, + customColor: customColor, +}; +}); diff --git a/src/client/view/internal.js b/src/client/view/internal.js new file mode 100644 index 00000000..bc18bd75 --- /dev/null +++ b/src/client/view/internal.js @@ -0,0 +1,14 @@ +define(['view/tab'], +function(tab) { + +var EDGE_INFO_SELECTOR = '.edge_info', + NODE_INFO_SELECTOR = '.info', + edit_tab = new tab.Tab({edge: EDGE_INFO_SELECTOR, node: NODE_INFO_SELECTOR}); + +return { + EDGE_INFO_SELECTOR: EDGE_INFO_SELECTOR, + NODE_INFO_SELECTOR: NODE_INFO_SELECTOR, + edit_tab: edit_tab, +}; + +}); diff --git a/src/client/view/node_info.js b/src/client/view/node_info.js new file mode 100644 index 00000000..44f2fab8 --- /dev/null +++ b/src/client/view/node_info.js @@ -0,0 +1,117 @@ +define(['jquery', 'jquery-ui', 'view/helpers', 'view/internal'], +function($, _unused_jquery_ui, view_helpers, internal) { + +var d = null, + submit_callback = null, + delete_callback = null; + +function _get_form_data() { + return { + name: $('.info #editformname').val(), + type: $('.info #edittype').val(), + url: $('.info #editurl').val(), + status: $('.info #editstatus').val(), + startdate: $("#editstartdate").val(), + enddate: $("#editenddate").val(), + }; +} + +//internal.edit_tab.get('node', "#editbox").submit(function(e) { +// if (submit_callback) { +// return submit_callback(e, _get_form_data()); +// } +// console.log('bug: edit tab submit called with no callback set'); +// e.preventDefault(); +//}) + +//internal.edit_tab.get('node', "#deletenode").click(function(e) { +// if (delete_callback) { +// return delete_callback(e, _get_form_data()); +// } +// console.log('bug: edit tab delete called with no callback set'); +// e.preventDefault(); +//}); + +function show(d) { + var info = $('.info'), + f = false, + t = true, + visible = { + "third-internship-proposal": [t, t, t, f, f], + "chainlink": [f, f, f, f, f], + "skill": [f, f, f, f, t], + "interest": [f, f, f, f, t], + "_defaults": [f, f, f, f, t], + }, + fields = ["#status", "#startdate", "#enddate", "#desc", "#url"], + flags = visible.hasOwnProperty(d.type) ? visible[d.type] : visible._defaults, + i; + + internal.edit_tab.show('node'); + + for (i = 0 ; i < flags.length; ++i) { + var elem = info.find(fields[i]); + elem[flags[i] ? 'show' : 'hide'](); + } + + $('.info').attr('class', 'info'); + $('.info').addClass('type-' + d.type); // Add a class to distinguish types for css + + $('.info').find('#editformname').val(d.name); + $("#editenddate").datepicker({ + inline: true, + showOtherMonths: true, + dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + }); + $("#editstartdate").datepicker({ + inline: true, + showOtherMonths: true, + dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + }); + + $('#editdescription').val(d.type); + + $('#edittype').val(d.type); + + $('#editurl').val(d.url); + + $('#editstatus').val(d.status); + + if (d.type === "third-internship-proposal") { + $('#editstartdate').val(d.start); + $('#editenddate').val(d.end); + } +} + +function hide() +{ + internal.edit_tab.hide(); +} + +function on_save(f) { + $('#edit-node-dialog__save').click(function(e) { + return f(e, _get_form_data()); + }); +} + +function on_delete(f) { + $('#edit-node-dialog__delete').click(function(e) { + return f(e, _get_form_data()); + }); +} + +function on_keyup(f) { + $('.info').keyup(function(e) { + return f(e, _get_form_data()); + }); +} + +return { + show: show, + hide: hide, + on_save: on_save, + on_delete: on_delete, + on_keyup: on_keyup, +}; + +}); diff --git a/src/client/view/selection.js b/src/client/view/selection.js new file mode 100644 index 00000000..d51d1a4b --- /dev/null +++ b/src/client/view/selection.js @@ -0,0 +1,102 @@ +define(['rz_core'], +function(rz_core) { + +function get_rz_core() +{ + // circular dependency on rz_core, so require.js cannot solve it. + if (rz_core === undefined) { + rz_core = require('rz_core'); + } + return rz_core; +} + +var selected_nodes = []; + +function byVisitors(node_selector, link_selector) { + var new_selected_nodes = get_rz_core().graph.findByVisitors(node_selector, link_selector); + + clear(); + connectedComponent(new_selected_nodes); +} + +function connectedComponent(nodes) { + var connected = get_rz_core().graph.getConnectedNodesAndLinks(nodes, 1), + i, + node, + link, + data; + + selected_nodes = nodes.map(function(x) { return x; }); + + for (i = 0 ; i < connected.nodes.length ; ++i) { + data = connected.nodes[i]; + node = data.node; + switch (data.type) { + case 'exit': + node.state = 'exit'; + break; + case 'enter': + node.state = 'enter'; + break; + }; + } + for (i = 0 ; i < connected.links.length ; ++i) { + data = connected.links[i]; + link = data.link; + switch (data.type) { + case 'exit': + link.state = 'exit'; + break; + case 'enter': + link.state = 'enter'; + break; + }; + } + nodes.forEach(function (n) { n.state = 'chosen'; }); +} + +var node_selected = function(node) { + return node.state == 'chosen' || node.state == 'enter' || node.state == 'exit' || node.state == 'selected' + || node.state == 'temp' || node.state == 'related'; +} + +var selected_class = function(node) { + return selected_nodes.length > 0 ? (node_selected(node) ? "selected" : "notselected") : ""; +} + +var clear = function() { + selected_nodes.length = 0; + get_rz_core().graph.setRegularState(); +} + +function arr_compare(a1, a2) +{ + if (a1.length != a2.length) { + return false; + } + for (var i = 0 ; i < a1.length ; ++i) { + if (a1[i] != a2[i]) { + return false; + } + } + return true; +} + +var update = function(nodes) { + var set = !arr_compare(nodes, selected_nodes); + clear(); + if (set) { + connectedComponent(nodes); + } +} + +return { + byVisitors: byVisitors, + connectedComponent: connectedComponent, + clear: clear, + update: update, + selected_class: selected_class, + node_selected: node_selected, +}; + +}); diff --git a/src/client/view/tab.js b/src/client/view/tab.js new file mode 100644 index 00000000..97ba1351 --- /dev/null +++ b/src/client/view/tab.js @@ -0,0 +1,57 @@ +"use strict" + +define(['jquery'], +function($) { + +function Tab(dict) { + var k, + selector = {}, + name = []; + + for (k in dict) { + if (dict.hasOwnProperty(k) == false) { + continue; + } + selector[k] = dict[k]; + name.push(k); + } + this._selector = selector; + this._name = name; +} + +Tab.prototype.show = function(shown_name) { + var i, + name, + element; + + for (i = 0 ; i < this._name.length ; ++i) { + name = this._name[i]; + element = $(this._selector[name]); + if (name === shown_name) { + element.fadeIn(300); + } else { + element.hide(); + } + } +} + +Tab.prototype.hide = function() { + var i; + + for (i = 0 ; i < this._name.length ; ++i) { + $(this._selector[this._name[i]]).fadeOut(300); + } +} + +Tab.prototype.get = function(name, sel) { + // selector concatenation + var e = $(this._selector[name] + ' ' + sel); + + return e; +} + +return { + Tab: Tab +}; + +}); diff --git a/src/client/view/timeline.js b/src/client/view/timeline.js new file mode 100644 index 00000000..0a26c0a7 --- /dev/null +++ b/src/client/view/timeline.js @@ -0,0 +1,180 @@ +"use strict" + +define(['jquery', 'rz_core'], +function ($, rz_core) { + +function checkSwitch(checkswitch) { + + if (checkswitch.checked) { + vis.selectAll(".timeline").remove(); + $('.missingdates').fadeOut(300); + scrollValue = $('body').scrollLeft(); + + $('body').scrollLeft(0); + graphstate = "GRAPH"; + rz_core.update_view__graph(); + + $('.status').fadeOut(600); + + //boxedin=false; + + } else { + timelineTimer=0; + $('.missingdates').fadeIn(300); + + graph.recenterZoom(); + + $('body').scrollLeft(scrollValue); + + graphstate = "TIMELINE"; + + rz_core.update_view__graph(); + + $('.status').fadeIn(600); + + + initAxis(); + + //boxedin=true; + } + +} + +///For some reason JQuery's $('body').scroll never worked, so I found something else. +$('body').bind('DOMMouseScroll', function(e){ + if(graphstate==="TIMELINE"){ + if(e.originalEvent.detail !== 0) { + $('.overlay').hide(); + }else{ + $('.overlay').show(); + } + }else{ + return false; + } + }); + + //IE, Opera, Safari + $('body').bind('mousewheel', function(e){ + if(graphstate==="TIMELINE"){ + if(e.originalEvent.wheelDelta !== 0) { + $('.overlay').hide(); + }else{ + $('.overlay').show(); + var left = $('body').offset().left; + $('.overlay').scrollLeft(left); + } + }else{ + return false; + } + }); + +function initAxis() { + var bar_height = 20; + var row_height = bar_height + 10; + var vertical_padding = 150 + var bar_start_offset = 40; + var h = 15 * row_height + vertical_padding; + + + var min = deliverables[0].startdate; + var max = deliverables[0].enddate; + + for (var i = 0; i < deliverables.length; i++) { + var deliv = deliverables[i]; + if (min > deliv.startdate) min = deliv.startdate; + if (end < deliv.enddate) end = deliv.enddate; + } + min = new Date(min); ///with min + max = new Date(max); + console.log("min "+min+" max "+max); + + ///update interval + var timeDiff = Math.abs(max.getTime() - min.getTime()); + var diffDays = timeDiff / (1000 * 3600 * 24); + var w = Math.round(diffDays * 15 - 100); + graphinterval = w / diffDays; + + var svg=vis; + vis.attr("width",w); + + var paddingLeft = 150; + var paddingTop = 120; + + var xScale = d3.time.scale() + .domain([min, max]).nice() + .range([paddingLeft, w]); + + var xAxis = d3.svg.axis() + .scale(xScale) + .orient("bottom"); + + // Lines + var line = svg.append("g") + .selectAll("line") + .data(xScale.ticks(40)) + .enter().append("line") + .attr("x1", xScale) + .attr("x2", xScale) + .attr("y1", paddingTop + 30) + .attr("y2", h - 50) + .attr("class", "timeline") + .style("stroke", "#ccc"); + + var y = function (i) { + return i * row_height + paddingTop + bar_start_offset; + } + + var labelY = function (i) { + return i * row_height + paddingTop + bar_start_offset + 13; + } + + // Company bars + var bar = svg.selectAll("rect") + .data(function (d) { + return Math.random() * 5; + }); + + bar.enter().append("rect") + .attr("y", -100) + .attr("x", -1000) + .attr("width", 100) + .attr("height", bar_height) + .attr("class", "company-bar timeline") + .on("mouseover", function (d) { + d3.select(this).style("fill", "#F5AF00"); + getCompanyData(String(d.uid)) + }) + .on("mouseout", function () { + d3.select(this).style("fill", "#fc0"); + }); + + var label = svg.selectAll("text") + .data(deliverables, function (key) { + return key.id + });; + + label.enter().append("text") + .attr("class", "bar-label timeline") + // .attr("text-anchor","end") + .attr("x", paddingLeft - 10) + .attr("y", function (d, i) { + return labelY(i); + }) + .text(function (d) { + }); + + // Bottom Axis + var btmAxis = svg.append("g") + .attr("transform", "translate(0," + (h - 25) + ")") + .attr("class", "axis timeline") + .call(xAxis); + + // Top Axis + var topAxis = svg.append("g") + .attr("transform", "translate(0," + paddingTop + ")") + .attr("class", "axis timeline") + .call(xAxis); +} + +return {}; +}); diff --git a/src/client/view/view.js b/src/client/view/view.js new file mode 100644 index 00000000..95bfc09b --- /dev/null +++ b/src/client/view/view.js @@ -0,0 +1,12 @@ +"use strict" + +define(['view/node_info', 'view/edge_info', 'view/internal'], +function(view_node_info, view_edge_info, view_internal) { +return { + 'node_info': view_node_info, + 'edge_info': view_edge_info, + 'hide': function() { + view_internal.edit_tab.hide(); + }, +}; +}); diff --git a/src/consts.js b/src/consts.js deleted file mode 100644 index 32167811..00000000 --- a/src/consts.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict" - -define(function() { - - var nodetypes = ["person", "club", "skill", "interest", "third-internship-proposal", "internship"]; - - var description = { - person: 'A person in CRI - student or teacher', - club: 'A shared club or project within the CRI', - skill: 'Ability or expertise you possess', - interest: 'Scientific skill or domain expertise you wish you had', - 'third-internship-proposal': 'Create this to submit your third internship proposal', - internship: "Title of first or second internship you've done" - }; - - // TODO: enums, sometime - return { - KEYSTROKE_WHERE_EDIT_NODE: 'keystroke_where_edit_node', - KEYSTROKE_WHERE_DOCUMENT: 'keystroke_where_document', - KEYSTROKE_WHERE_TEXTANALYSIS: 'keystroke_where_textanalysis', - INPUT_WHERE_TEXTANALYSIS: 'input_where_textanalysis', - nodetypes: nodetypes, - description: description, - }; -}); diff --git a/src/drag_n_drop.js b/src/drag_n_drop.js deleted file mode 100644 index da166ad4..00000000 --- a/src/drag_n_drop.js +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index ab864d1c..00000000 --- a/src/history.js +++ /dev/null @@ -1,149 +0,0 @@ -"use strict" -// Once upon a time we shall have a versioned property graph from which history -// will be one extractable aspect, much like a git for graphs. Now we just have -// a plain list of events for a specific user. - -// Enums and chrome don't play along well. Object.freeze I guess? actually rhizi code cuases -// exceptions but that shouldn't break the console, as evidenced by the '__commandLineAPI is not defined' -// error below. -// -// Uncaught TypeError: Can't add property addednodes, object is not extensible rz_core.js:4 -// Uncaught TypeError: Can't add property text, object is not extensible textanalysis.js:3 -// Uncaught TypeError: Can't add property key, object is not extensible buttons.js:5 -// Uncaught ReferenceError: sentence is not defined robot.js:11 -// Resource interpreted as Font but transferred with MIME type application/font-sfnt: "http://localhost:8000/external/Lato300.ttf". jquery.js:2 -// > document -// ReferenceError: __commandLineAPI is not defined -//var ActionEnum = Enum(); - -define(['jquery', 'FileSaver', 'consts', 'rz_bus'], - function($, saveAs, consts, rz_bus) { - -/* user - username (string) - * svg - svg element for catching zoom events (jquery DOMNode wrapper) - */ -function History(user, graph, transform_element) { - var that = this; - this.records = []; - this.user = user; - this.transform_element = transform_element; - graph.diffBus.onValue(function (obj) { - return that.record_graph_diff(obj) - }); - rz_bus.ui_key.onValue(that.record_keystrokes.bind(that)); - rz_bus.ui_input.onValue(that.record_input.bind(that)); - // XXX create zoom behavior - then proof to event name change - $(window).on('wheel.history', function(obj) { - that.record_zoom(obj); - return true; - }); -} - -var ACTION_KEYSTROKES = 'ACTION_KEYSTROKES'; -var ACTION_INPUT = 'ACTION_INPUT'; -var ACTION_GRAPH_DIFF = 'ACTION_GRAPH_DIFF'; -var ACTION_ZOOM = 'ACTION_ZOOM'; - -var KEYSTROKE_WHERE_TEXTANALYSIS = 'KEYSTROKE_WHERE_TEXTANALYSIS'; -var KEYSTROKE_WHERE_DOCUMENT = 'KEYSTROKE_WHERE_DOCUMENT'; -var KEYSTROKE_WHERE_EDIT_NODE = 'KEYSTROKE_WHERE_EDIT_NODE'; - -History.prototype.record = function(action, d) -{ - if (d === undefined || action === undefined) { - throw "Invalid arguments"; - } - d['action'] = action; - d['user'] = this.user; - d['timestamp'] = new Date(); - this.records.push(d); - $('.history-timeline').html('
' + JSON.stringify(d) + '
'); -}; - -function svg_extract_translate_and_scale(e) -{ - // See: http://stackoverflow.com/questions/10349811/how-to-manipulate-translate-transforms-on-a-svg-element-with-javascript-in-chrom - // Using the regexp option right now, did only firefox testing 36 - var str = e.attributes['transform'].value; - var parts = /translate\(\s*([^\s,)]+)[ ,]([^\s,)]+)/.exec(str); - var scale = /scale\(\s*([^\s)]+)\)/.exec(str); - if (scale) { - var x = parts[1], y = parts[2]; - return {scale:+scale[1], translate: [+x, +y]}; - } else { - return {scale:1.0, translate: [0.0, 0.0]}; - } -} - -History.prototype.record_zoom = function(d) -{ - var transform = svg_extract_translate_and_scale(this.transform_element); - - if (transform === undefined) { - console.log('record_zoom: bug: transform_element has no transform attribute'); - return; - } - this.record(ACTION_ZOOM, {transform: transform}); -} - -History.prototype.save_to_file = function() -{ - var json = JSON.stringify(this.records, function (k, v) { return v; }, 2); - - saveAs(new Blob([json], {type: 'application/json'}), 'history.json'); -}; - -History.prototype.clear = function() -{ - this.records = []; -} - -History.prototype.record_graph_diff = function(obj) -{ - this.record(ACTION_GRAPH_DIFF, { - nodes: {add: obj.nodes && obj.nodes.add, remove: obj.nodes && obj.nodes.remove, - change: obj.nodes && obj.nodes.change}, - links: {add: obj.links && obj.links.add, remove: obj.links && obj.links.remove, - change: obj.links && obj.links.change}, - }); -} - -History.prototype.record_keystrokes = function(obj) -{ - var where = obj.where, - keys = obj.keys; - - if (where === undefined || keys === undefined || keys.length === undefined || - keys.length <= 0) { - console.log("invalid arguments"); - return; - } - keys = keys.filter(function(k) { return k !== undefined; }); - if (keys.length == 0) { - return; - } - this.record(ACTION_KEYSTROKES, { - keys: keys, - where: where - }); -} - -History.prototype.record_input = function(obj) -{ - var where = obj.where, - input = obj.input; - - if (where === undefined || input === undefined || input.length === undefined || typeof input !== 'string') { - console.log('invalid arguments'); - return; - } - this.record(ACTION_INPUT, {where: where, input: input}); -} - -return { - History:History, - KEYSTROKE_WHERE_TEXTANALYSIS:KEYSTROKE_WHERE_TEXTANALYSIS, - KEYSTROKE_WHERE_DOCUMENT:KEYSTROKE_WHERE_DOCUMENT, - KEYSTROKE_WHERE_EDIT_NODE:KEYSTROKE_WHERE_EDIT_NODE -}; -}); // define diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 863d0440..00000000 --- a/src/main.js +++ /dev/null @@ -1,94 +0,0 @@ -define(['textanalysis.ui', 'textanalysis', 'buttons', 'history', 'drag_n_drop', 'robot', 'model/core', 'rz_config', 'rz_core', 'view/selection', 'util', 'view/completer'], -function(textanalysis_ui, textanalysis, buttons, history, drag_n_drop, robot, model_core, rz_config, rz_core, selection, util, completer) { - - function expand(obj){ - if (!obj.savesize) { - obj.savesize = obj.size; - } - obj.size = Math.max(obj.savesize, obj.value.length); - } - - this.main = function() { - var json, - search = $('#search'), - search_completer = completer(search, $('#search-suggestion'), - {triggerStart:' ', triggerEnd:' '}); - - console.log('Rhizi main started'); - search_completer.options.plug(textanalysis.suggestions_options); - drag_n_drop.init(); - $('#editname').onkeyup = function() { expand(this); }; - $('#editlinkname').onkeyup = function() { expand(this); }; - $('#textanalyser').onkeyup = function() { expand(this); }; - - textanalysis_ui.main(); - - json = util.getParameterByName('json'); - if (json) { - rz_core.load_from_json(json); - } - if (util.getParameterByName('debug')) { - $(document.body).addClass('debug'); - rz_core.graph.set_user('fakeuser'); - } - - document.body.onkeyup = function(e) { - var key = (e.key || (e.charCode && String.fromCharCode(e.charCode)) - || (e.which && String.fromCharCode(e.which))).toLowerCase(); - - if (e.altKey && e.ctrlKey && key == 'i') { - $('#textanalyser').focus(); - } - if (e.altKey && e.ctrlKey && key == 'o') { - search.focus(); - } - if (e.ctrlKey && key == 'z' && e.target.nodeName !== 'INPUT') { - // TODO: rz_core.graph.undo(); - } - }; - // TODO: move me somewhere - function search_on_submit() { - var text = search[0].value.trim(), - r; - - try { - r = new RegExp(text.replace(/ /, '|'), 'i'); - } catch (e) { - return; // don't clear selection either - } - if (text.length > 0) { - selection.byVisitors(function (n) { return n.name.match(r); }); - } else { - selection.clear(); - } - rz_core.update_view__graph(false); - }; - search.on('input', search_on_submit); - search.on('keydown', function(e) { - if (e.which == 13 && !search_completer.handleEnter()) { - e.preventDefault(); - search_on_submit(e); - return false; - } - return undefined; - }); - - var intro_task_elem = $('#intro-task'); - // TODO: messages (why tasks?) - this one is special but we want them to be handled in their own file. - if (!localStorage.intro_task_hide) { - intro_task_elem.show(); - } - $('#intro-task .task-close-button').click(function(e) { - localStorage.intro_task_hide = true; - intro_task_elem.hide(); - }); - - // TODO: interaction between the hack above and this - model_core.init(rz_config); - textanalysis.init(rz_core.graph); - } - - return { - main: main }; - } -); diff --git a/src/model/core.js b/src/model/core.js deleted file mode 100644 index 5c643ea1..00000000 --- a/src/model/core.js +++ /dev/null @@ -1,187 +0,0 @@ -"use strict" - -/** - * core model module - currently unused - */ -define(['util'], function(util) { - - /** - * return a random id - */ - var random_id; - - var random_id__hash = function() { - return Math.random().toString(36).substring(2, 10); - } - - var random_id__seq = function () { - var id = 0; - function get_next() { - var next = id; - id += 1; - return next; - } - return get_next; - } - - function random_node_name() { - return random_id__hash(); - } - - function init(config){ - if (config['rand_id_generator'] == 'hash') { - random_id = random_id__hash; - } - if (config['rand_id_generator'] == 'seq') { - random_id = random_id__seq(); - } - } - - function Node() { - } - Node.prototype.equals = function(other_node){ - return this.id == other_node.id; - } - - function Link() { - } - // adapte Link to force_layoutL create __src,__dst aliases - Link.prototype.__defineGetter__('source', function(){ - return this.__src; - }); - Link.prototype.__defineGetter__('target', function(){ - return this.__dst; - }); - - /** - * the most flexible way to create a node: - perform spec field validation - - * fill-in missing spec fields - */ - function create_node_from_spec(node_spec) { - var ret = new Node(); - - if (undefined != node_spec.id) { - // reuse id if present - __set_obj_id(ret, node_spec.id); - } - - util.assert(undefined != node_spec.name, 'create_node_from_spec: name missing'); - - ret.name = node_spec.name; - - // type - if (undefined == node_spec.type) { - console.debug('create_node_from_spec: undefined type, falling back to \'empty\''); - node_spec.type = 'empty'; - } - ret.type = node_spec.type; - - // status - ret.status = node_spec.status || 'unknown'; - - // visual - ret.x = node_spec.x; - ret.y = node_spec.y; - - // other - ret.state = node_spec.state; - ret.url = node_spec.url; - ret.start = node_spec.start; - ret.end = node_spec.end; - - return ret; - } - - function __set_obj_id(obj, id) { - Object.defineProperty(obj, "id", { - value: id, - enumerable: true, - writable: false - }); - } - - /** - * @param node_spec: id must not be defined - */ - function create_node__set_random_id(node_spec) { - if (undefined == node_spec) { - node_spec = {}; - } - - var ret = create_node_from_spec(node_spec); - - util.assert(undefined == ret.id); // id must not be defined in spec - __set_obj_id(ret, random_id()); - - return ret; - } - - function create_link__set_random_id(src, dst, link_spec) { - var ret = create_link_from_spec(src, dst, link_spec); - __set_obj_id(ret, random_id()); - return ret; - } - - /** - * determine if nodes are equal by name - * - * @param other_node - * @returns {Boolean} - */ - Node.prototype.equal_by_name = function(other) { - ret = this.name.toLowerCase() == other.name.toLowerCase(); - if (false == ret) { - console.debug(this.id + ' != ' + other.id); - } - return ret; - } - - function create_link_from_spec(src, dst, link_spec) { - var ret = new Link(); - - if (undefined != link_spec.id) { - // reuse id if present - __set_obj_id(ret, link_spec.id); - } - - util.assert(undefined != src, 'create_link_from_spec: src missing'); - util.assert(undefined != dst, 'create_link_from_spec: dst missing'); - util.assert(undefined != src.id, 'create_link_from_spec: src missing id'); - util.assert(undefined != dst.id, 'create_link_from_spec: dst missing id'); - util.assert(undefined != link_spec.name, 'create_link_from_spec: name missing, unable to deduce type'); - - ret.__src = src; - ret.__dst = dst; - ret.__type = link_spec.name; - - if (undefined == link_spec.name){ - console.warn('create_link_from_spec: name: ' + link_spec.name); - link_spec.name = ""; - } - ret.name = link_spec.name.trim(); - - ret.state = link_spec.state; - return ret; - } - - /** - * determine if links are equal by ID - * - * @param other_node - * @returns {Boolean} - */ - Link.prototype.equal_by_id = function(other) { - return this.id.toLowerCase() == other.id.toLowerCase(); - } - - return { - init : init, - Node: Node, // allow model adaptation - Link: Link, // allow model adaptation - random_node_name : random_node_name, - create_node_from_spec : create_node_from_spec, - create_node__set_random_id : create_node__set_random_id, - create_link_from_spec : create_link_from_spec, - create_link__set_random_id : create_link__set_random_id, - }; -}); diff --git a/src/model/diff.js b/src/model/diff.js deleted file mode 100644 index 8903b161..00000000 --- a/src/model/diff.js +++ /dev/null @@ -1,187 +0,0 @@ -"use strict" - -/** - * Diff module - */ -define([], - function() { - - /** - * A set of diff objects - */ - function Diff_Set(obj_spec) { - this.__diff_set_topo = []; - this.__diff_set_attr = []; - this.__diff_set_vis = []; - } - Diff_Set.prototype.add_diff_obj = function(diff_obj) { - if (diff_obj instanceof Topo_Diff) { - this.__diff_set_topo.push(diff_obj); - } - if (diff_obj instanceof Attr_Diff) { - this.__diff_set_attr.push(diff_obj); - } - if (diff_obj instanceof Vis_Diff) { - this.__diff_set_vis.push(diff_obj); - } - } - - /** - * Topological diff object - */ - function Topo_Diff(obj_spec) { - - this.link_set_rm = obj_spec.link_set_rm; - this.node_set_rm = obj_spec.node_set_rm; - this.node_set_add = obj_spec.node_set_add; - this.link_set_add = obj_spec.link_set_add; - - } - Topo_Diff.prototype.for_each_node_add = function(callback, this_arg) { - this.node_set_add.forEach(callback, this_arg); - } - - Topo_Diff.prototype.for_each_node_rm = function(callback, this_arg) { - this.node_set_rm.forEach(callback, this_arg); - } - - Topo_Diff.prototype.for_each_link_add = function(callback, this_arg) { - this.link_set_add.forEach(callback, this_arg); - } - - Topo_Diff.prototype.for_each_link_rm = function(callback, this_arg) { - this.link_set_rm.forEach(callback, this_arg); - } - - /** - * Attribute diff object, organized by type, where currently - * node,link types are supported - */ - function Attr_Diff(obj_spec) { - this.__type_node = {}; - this.__type_link = {}; - } - - Attr_Diff.prototype.init_attr_diff = function(type_name, id) { - - if ('node' != type_name && 'link' != type_name) { - console.error('attempt to init attribute diff for unsupported type: ' + type_name); - return; - } - - var type_field = '__type_' + type_name; - this[type_field][id] = { - '__attr_write' : {}, - '__attr_remove' : [] - }; - - return this; - } - - Attr_Diff.prototype.init_attr_diff_node = function(id) { - return this.init_attr_diff('node', id); - } - - Attr_Diff.prototype.init_attr_diff_link = function(id) { - return this.init_attr_diff('link', id); - } - - Attr_Diff.prototype.add_node_attr_write = function(n_id, attr_name, - attr_val) { - - if (undefined == this.__type_node[n_id]) { - this.init_attr_diff_node(n_id); - } - this.__type_node[n_id].__attr_write[attr_name] = attr_val; - return this; - } - - Attr_Diff.prototype.add_node_attr_rm = function(n_id, attr_name) { - if (undefined == this[n_id]) { - this.init_attr_diff(n_id); - } - this.__type_node[n_id].__attr_remove.push(attr_name); - return this; - } - - Attr_Diff.prototype.add_link_attr_write = function(l_id, attr_name, - attr_val) { - - if (undefined == this.__type_link[l_id]) { - this.init_attr_diff_link(l_id); - } - this.__type_link[l_id].__attr_write[attr_name] = attr_val; - return this; - } - - Attr_Diff.prototype.add_link_attr_rm = function(l_id, attr_name) { - if (undefined == this[l_id]) { - this.init_attr_diff(l_id); - } - this.__type_link[l_id].__attr_remove.push(attr_name); - return this; - } - - /** - * Visual diff object expressing any visual change to the state of a - * particular visualization type. - * - * @obj_spec if none is passed a default topo_diff is constructed - * with node,link add sets - */ - function Vis_Diff(obj_spec) { - } - - function new_topo_diff(obj_spec) { - /* - * validate obj_spec - */ - var ret; - if (undefined == obj_spec) { - obj_spec = { - node_set_add : [], - link_set_add : [], - } - ret = new Topo_Diff(obj_spec); - } else { - ret = new Topo_Diff(obj_spec); - } - return ret; - } - - function new_attr_diff(obj_spec) { - /* - * validate obj_spec - */ - // TODO - var ret = new Attr_Diff(obj_spec); - ret.__type_node = {}; // id-to-obj map - ret.__type_link = {}; // id-to-obj map - return ret; - } - - function new_vis_diff(obj_spec) { - /* - * validate obj_spec - */ - // TODO - var ret = new Vis_Diff(obj_spec); - return ret; - } - - function new_diff_set(obj_spec) { - /* - * validate obj_spec - */ - // TODO - var ret = new Diff_Set(obj_spec); - return ret; - } - - return { - new_topo_diff : new_topo_diff, - new_attr_diff : new_attr_diff, - new_vis_diff : new_vis_diff, - new_diff_set : new_diff_set, - } - }); \ No newline at end of file diff --git a/src/model/graph.js b/src/model/graph.js deleted file mode 100644 index b3c8e33f..00000000 --- a/src/model/graph.js +++ /dev/null @@ -1,873 +0,0 @@ -"use strict" - -define(['Bacon', 'consts', 'util', 'model/core', 'model/util', 'model/diff', 'rz_api_backend', 'rz_api_mesh', 'history', 'rz_bus', 'rz_config'], -function (Bacon, consts, util, model_core, model_util, model_diff, rz_api_backend, rz_api_mesh, history, rz_bus, rz_config) { - -var debug = false; - -function Graph() { - - var nodes = [], - id_to_node_map = {}, - links = [], - diffBus = new Bacon.Bus(); - - this.diffBus = diffBus; - - /** - * add node if no previous node is present whose id equals that of the node being added - * - * @return node if node was actually added - */ - this.addNode = function(spec) { - var node = this.__addNode(spec); - if (node) { - return node; - } - } - - /** - * Inner implementation - * - * @param notify whether or not a presenter notification will be sent, default = true - */ - function __addNode(spec, notify, peer_notify) { - var existing_node, - node; - - notify = undefined === notify ? true : notify; - peer_notify = undefined === peer_notify ? true : peer_notify; - - if (undefined == spec.id) { - existing_node = findNodeByName(spec.name) - if (existing_node){ - return existing_node; - } else { - node = model_core.create_node__set_random_id(spec); - if (debug) { - if ('bubble' != node.type){ - console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\' (bubble)'); - }else { - console.log('__addNode: stamping node id: ' + node.id + ', name: \'' + node.name + '\''); - } - } - } - } else { - node = model_core.create_node_from_spec(spec); - } - - existing_node = find_node__by_id(node.id); - if (existing_node) { - console.log('__addNode: id collision: existing-node.id: \'' + existing_node.id + '\', ' + 'new-node.id: \'' + node.id + '\''); - return existing_node; - } - - util.assert(undefined != node.id, '__addNode: node id missing'); - nodes.push(node); - id_to_node_map[node.id] = node; - console.log('__addNode: node added: id: ' + node.id); - - if (rz_config.backend_enabled && peer_notify){ - var topo_diff = model_diff.new_topo_diff({ - node_set_add : [node].map(model_util.adapt_format_write_node), - }); - var on_success = function(){ - // FIXME: handle possible outcomes: - // - id merge: node already exists -> update id - // - link-merge: node already exists -> merge links, recurse? - }; - var on_error = function(){ - // TODO: add problem emblem to node - }; - rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error); - } - - if (notify) { - diffBus.push({nodes: {add: [node]}}); - } - - return node; - } - this.__addNode = __addNode; - - this._remove_node_set = function(ns, peer_notify) { - - peer_notify = undefined === peer_notify ? true : peer_notify; - - var cascade_link_rm_set = []; // track cascading link removals - for (var j = 0; j < ns.length; j++) { - var n = ns[j]; - var i = 0; - while (i < links.length) { - var link = links[i]; - if ((link['__src'].equals(n)) || (link['__dst'].equals(n))) { // compare by id - links.splice(i, 1); - cascade_link_rm_set.push(link); - } - else { - i++; - } - } - var index = findNodeIndex(n.id, n.state); - if (index !== undefined) { - nodes.splice(index, 1); - - util.assert(undefined != n.id, '_remove_node_set: node id missing'); - delete id_to_node_map[n.id]; - } - } - - cascade_link_rm_set.forEach(function(n){ - console.log('_remove_node_set: removed node: id: ' + n.id); - }); - - if (rz_config.backend_enabled && peer_notify){ - var topo_diff = model_diff.new_topo_diff({ - node_set_rm : ns.map(function(n){ return n.id; }), - link_set_rm : cascade_link_rm_set.map(function(l){ return l.id; }), - }); - var on_success = function(){ - // FIXME: handle possible outcomes: - // - rm cascade of connected links - }; - var on_error = function(){ - // TODO: add problem emblem to node - }; - rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error); - } - - if (ns.length > 0) { - diffBus.push({nodes: {removed: ns.map(function(n) { return n.id; })}}); - } - } - - this.removeNode = function(id) { - var n = find_node__by_id(id); - this._remove_node_set([n]); - } - - this.removeNodes = function(n_filer) { - var ns = find_node_set_by_filer(n_filer); - this._remove_node_set(ns); - } - - /** - * - * getConnectedNodesAndLinks - * - * @id - * @state - defines the starting node (must have id and state) - * @d - depth defining connected component. If -1 returns the entire connected component. (can be the whole graph) - * - * NOTE: chainlinks are treated specially, they don't count for distance. So all their decendants will be added. - * - * NOTE: temp state nodes (n.state === 'temp') are ignored. - * - * @return - { - * 'node': [node] - * 'link': [link] - * } - * - * TODO: rewrite using efficient data structure. Right now iterates over everything - * TODO: implement for d !== 1 - * - */ - this.getConnectedNodesAndLinks = function(chosen_nodes, d) { - var ret = {'nodes':[], 'links':[]}; - - function addNode(node) { - if (chosen_nodes.filter(function (n) { return n.id == node.id; }).length == 1) { - return; - } - ret.nodes.push(node); - } - function same(n1, n2) { - // XXX: using name comparison because one of the nodes might be stale - return compareNames(n1.name, n2.name); - } - - if (chosen_nodes === undefined) { - console.log('getConnectedNodesAndLinks: bug: called with undefined node'); - return; - } - if (d !== 1) { - console.log('getConnectedNodesAndLinks: bug: not implemented for d == ' + d); - } - d = d || 1; - - if (chosen_nodes.length === undefined) { - console.log('getConnectedNodesAndLinks: expected array'); - } - - links.forEach(function(link) { - chosen_nodes.forEach(function (n) { - var adjacentnode; - if (same(link.__src, n)) { - adjacentnode = find_node__by_id(link.__dst.id); - if (adjacentnode.state !== "temp") { - addNode({type: 'exit', node: adjacentnode}); - } - ret.links.push({type: 'exit', link: link}); - if (link.__dst.type === "chainlink") { - links.forEach(function(link2) { - if (link.__dst.id === link2.__dst.id && - link2.__dst.type === "chainlink" && - link2.__dst.state !== "temp") { - adjacentnode = find_node__by_id(link2.__src.id); - if (adjacentnode.state !== "temp") { - addNode({type: 'enter', node: adjacentnode}); - } - ret.links.push({type: 'enter', link: link2}); - } - }); - } - } - if (same(link.__dst, n)) { - adjacentnode = find_node__by_id(link.__src.id); - if (adjacentnode.state !== "temp") { - addNode({type: 'enter', node: adjacentnode}); - } - ret.links.push({type: 'enter', link: link}); - } - }); - }); - return ret; - } - - /* compareSubset: - * state: one of the optional states that defines a subgraph - * new_nodes: array of objects with name - * new_links: array of length two arrays [source_name, target_name] - * returns: true if current and new graph are homomorphic up to - * a single node id change. false otherwise - */ - this.compareSubset = function(state, new_nodes, new_links) { - // Note: the nodes include a state=='temp', type=='bubble' node - // but it's ok since it exists both in new_nodes and in state_nodes - var state_nodes = findNodes(null, state).filter(function (nd) { - return nd.type !== 'bubble'; - }); - var state_links = findLinks(state).map(function(link) { - return [link.__src.name, link.__dst.name]; - }).sort(); - var k; - var state_source, state_target, new_source, new_target; - var changed_nodes; - var verbose = false; // XXX should be global. - var set_old_name, set_new_name; - - new_nodes.map(function (f) { - if (!f.name) { - console.log('missing name on node. node follows'); - console.log(f); - } - }); - new_nodes.sort(); - new_links.sort(); - if (new_nodes.length != state_nodes.length || new_links.length != state_links.length) { - if (verbose) { - console.log('not same size: new/old ' + new_nodes.length + ' / ' + state_nodes.length + '; ' + - new_links.length + ' / ' + state_links.length); - } - return {graph_same: false}; - } - changed_nodes = util.set_diff(util.set_from_array(state_nodes.map(function(d) { return d.name; })), - util.set_from_array(new_nodes.map(function (f) { return f.name; }))); - // we allow any number of changed nodes as long as we it is 1 or 2 :) - if (changed_nodes.a_b.length > 2) { - if (verbose) { - console.log('changed too many nodes'); - console.log(changed_nodes); - } - return {graph_same: false}; - } - set_old_name = util.set_from_array(changed_nodes.a_b); - set_new_name = util.set_from_array(changed_nodes.b_a); - for (k = 0 ; k < state_links.length ; ++k) { - state_source = state_links[k][0]; - state_target = state_links[k][1]; - new_source = new_links[k][0]; - new_target = new_links[k][1]; - if ((state_source !== new_source && - !(state_source in set_old_name && new_source in set_new_name)) - || - (state_target !== new_target && - !(state_target in set_old_name && new_target in set_new_name))) { - if (verbose) { - console.log('not same link: ' + - state_source + '->' + state_target + ' != ' + - new_source + '->' + new_target); - console.log('state_source === new_source: ' + String(state_source === new_source)); - console.log('state_target === new_target: ' + String(state_target === new_target)); - console.log(set_old_name); - console.log(set_new_name); - } - return {graph_same: false}; - } - } - return {graph_same: true, old_name: changed_nodes.a_b, new_name: changed_nodes.b_a}; - } - - this.addLinkByName = function(src_name, dst_name, name, state, drop_conjugator_links) { - - var src = findNodeByName(src_name), - dst = findNodeByName(dst_name), - src_id = src ? src.id : null, - dst_id = dst ? dst.id : null; - - if (src_id === null || dst_id === null) { - console.log('error: link of missing nodes: ' + src_name + ' (' + src_id + ') -> ' - + dst_name + ' (' + dst_id + ')'); - return; - } - - var link = model_core.create_link__set_random_id(src, dst, { name: name, - state: state }); - this.addLink(link); - } - - function addLink(link, peer_notify) { - - util.assert(link instanceof model_core.Link); - - peer_notify = undefined === peer_notify ? true : peer_notify; - - var existing_link = findLink(link.__src.id, link.__dst.id, link.name); - - if (undefined == existing_link) { - - links.push(link); - - if (rz_config.backend_enabled && peer_notify){ - var topo_diff = model_diff.new_topo_diff({ - link_set_add : [link].map(model_util.adapt_format_write_link), - }); - var on_success = function(){ - // FIXME: handle possible outcomes: - // - id merge: link already exists -> update id - // - attr-merge: link already exists -> merge attrs - }; - var on_error = function(){ - // TODO: add problem emblem to node - }; - rz_api_backend.commit_diff__topo(topo_diff, on_success, on_error); - } - - diffBus.push({links: {add: [link]}}); - } else { - existing_link.name = link.name; - existing_link.state = link.state; - } - } - this.addLink = addLink; - - this.editLink = function(src_id, dst_id, newname, newstate) { - var link = findLink(src_id, dst_id, newname); - - if (link === undefined) { - return; - } - link.name = newname; - if (newstate !== undefined) { - link.state = newstate; - } - rz_bus.names.push([newname]); - } - - this.editLinkTarget = function(src_id, dst_id, new_dst_id) { - var link = findLink(src_id, dst_id, null); - if (link !== undefined) { - link.__dst = find_node__by_id(new_dst_id); - - } else { - - } - } - - this.update_node = function(node, new_node_spec, on_success, on_error) { - util.assert(node instanceof model_core.Node); - - if (rz_config.backend_enabled){ - - if (node.name != new_node_spec.name){ - /* - * handle name update collision: suggest removal first - */ - var n_eq_name = findNodeByName(new_node_spec.name); - if (undefined != n_eq_name) { - // delete colliding node on rename - console.warn('update_node: name collision blocked due to node rename'); - undefined != on_error && on_error(); - return; - } - - node['name'] = new_node_spec['name']; // [!] may still fail due to server NAK - } - - var attr_diff = model_diff.new_attr_diff(); - for (var key in new_node_spec){ - attr_diff.add_node_attr_write(node.id, key, new_node_spec[key]); - } - - var on_ajax_success = function(id_to_node_map){ - var node_id = node.id; // original node id - if (id_to_node_map[node_id].id != node_id){ - // TODO: handle incoming ID update - util.assert(false, 'update_node: id attr change'); - } - - var ret_node = id_to_node_map[node_id]; - for (var key in ret_node){ - if ('name' == key || 'id' == key){ - continue; - } - node[key] = ret_node[key]; - } - - // TODO: handle NAK: add problem emblem to node - on_success(); - }; - - var on_ajax_error = function(){ - }; - - rz_api_backend.commit_diff__attr(attr_diff, on_ajax_success, on_ajax_error); - } - } - - this.editNameByName = function(old_name, new_name) { - var node = findNodeByName(old_name); - - if (node === undefined) { - console.log('editNameByName: error: cannot find node with name ' + old_name); - return; - } - return this.editName(node.id, new_name); // TODO: introduce Node class (yes Amir, I'm now down with that). - } - - this.editName = function(id, new_name) { - var n_eq_name = findNodeByName(new_name); - var n_eq_id = find_node__by_id(id); - var acceptReplace=true; - - if (n_eq_id === undefined) { - return; - } - if (n_eq_id.name == new_name) { - return; - } - if (n_eq_name !== undefined && n_eq_id.state !== 'temp' && !compareNames(n_eq_id.name, new_name)) { - acceptReplace = confirm('"' + n_eq_name.name + '" will replace "' + n_eq_id.name + '", are you sure?'); - if (acceptReplace){ - for (var i = 0; i < links.length; i++) { - if (links[i].__src === n_eq_id) { - links[i].__src = n_eq_name; - } - if (links[i].__dst === n_eq_id) { - links[i].__dst = n_eq_name; - } - } - this.removeNode(n_eq_id.id); - } - } else { - n_eq_id.name = new_name; - } - } - - this.editDates = function(id, state, start, end) { - var n = find_node__by_id(id); - if (state != n.state){ - return; - } - if ((n !== undefined)) { - n.start = start; - n.end = end; - } - } - - /** - * editType: - * - * @return true if type changed - */ - this.editType = function(id, state, newtype) { - return this._editProperty(id, state, 'type', newtype); - } - - this.editURL = function(id, state, url) { - return this._editProperty(id, state, 'url', url); - } - - this._editProperty = function(id, state, prop, value) { - var n = find_node__by_id(id); - if (state != n.state){ - return false; - } - - if ((n === undefined)) { - return false; - } - n[prop] = value; - return true; - } - - this.editStatus = function(id, state, status) { - return this._editProperty(id, state, 'status', status); - } - - this.editState = function(id, state, newstate) { - return this._editProperty(id, state, 'state', newstate); - } - - this.findCoordinates = function(id) { - var n = find_node__by_id(id); - if ((index !== undefined)) { - $('.typeselection').css('top', n.y - 90); - $('.typeselection').css('left', n.x - 230); - } - } - - this.removeLink = function(link) { - var i; - - for (i = 0 ; i < links.length; ++i) { - if (link.id !== undefined) { - if (link.id === links[i].id) { - links.splice(i, 1); - return; - } - } else { - if (link.__src.id === links[i].__src.id && link.__dst.id === links[i].__dst.id) { - links.splice(i, 1); - return; - } - } - } - console.log('bug: attempt to remove non existant link'); - } - - this.removeLinks = function(state) { - var id = null; - var ls = findLinks(state); - for (var j = 0; j < ls.length; j++) { - var l = ls[j]; - var i = 0; - while (i < links.length) { - if (links[i] === l) links.splice(i, 1); - else i++; - } - } - } - - var findLink = function(src_id, dst_id, name) { - for (var i = 0; i < links.length; i++) { - if (links[i].__src.id === src_id && links[i].__dst.id === dst_id) { - return links[i]; - } - } - } - - var findLinks = function(state) { - var foundLinks = []; - for (var i = 0; i < links.length; i++) { - if (links[i].state == state) { - foundLinks.push(links[i]); - } - } - return foundLinks; - } - - var compareNames = function(name1, name2) { - return name1.toLowerCase() === name2.toLowerCase(); - }; - - var hasNodeByName = function(name, state) { - return nodes.filter(function (n) { - return compareNames(n.name, name) && n.state === state; - }).length > 0; - } - this.hasNodeByName = hasNodeByName; - - var hasNodeByNameAndNotState = function(name, state) { - return nodes.filter(function(n) { - return compareNames(n.name, name) && n.state !== state; - }).length > 0; - } - this.hasNodeByNameAndNotState = hasNodeByNameAndNotState; - - var hasNode = function(id, state) { - var i; - - for (i = 0 ; i < nodes.length; ++i) { - if (nodes[i].id === id && nodes[i].state === state) { - return true; - } - } - return false; - } - this.hasNode = hasNode; - - /** - * return node whose id matches the given id or undefined if no node was found - */ - var find_node__by_id = function(id) { - return id_to_node_map[id]; - } - - /** - * @param filer: must return true in order for node to be included in the returned set - */ - var find_node_set_by_filer = function(filter) { - var ret = []; - nodes.map(function(n){ - if (true == filter(n)){ - ret.push(n); - } - }); - return ret; - } - - var findNodeByName = function(name) { - for (var i = 0 ; i < nodes.length ; ++i) { - if (compareNames(nodes[i].name, name)) { - return nodes[i]; - } - } - } - - var findNodes = function(id, state) { - // id=id.toLowerCase(); - var foundNodes = []; - for (var i = 0; i < nodes.length; i++) { - if ((id && nodes[i].id === id) || (state && nodes[i].state === state)) - foundNodes.push(nodes[i]); - } - return foundNodes; - } - - var findNodeIndex = function(id, state) { - for (var i = 0; i < nodes.length; i++) { - if ((id && nodes[i].id === id) || (state && nodes[i].state === state)) - return i; - }; - } - - function clear() { - nodes.length = 0; - links.length = 0; - } - this.clear = clear; - - function empty() { - return nodes.length == 0 && links.length == 0; - } - this.empty = empty; - - // @ajax-trans - this.commit_diff_set = function (diff_set) { - - function on_success(data){ - console.log('commit_diff_set:on_success: TODO impl'); - } - - rz_api_mesh.broadcast_possible_next_diff_block(diff_set); - } - - /** - * perform initial DB load from backend - * - * @param on_success: should be used by MVP presentors to trigger UI update - */ - // @ajax-trans - function load_from_backend(on_success) { - - function on_success__ajax(data){ - var n_set = []; // added node set - var l_set = []; // added link set - var len; - - data['node_set'].map(function(n_spec) { - n_spec = model_util.adapt_format_read_node(n_spec); - - util.assert(undefined != n_spec.id, 'load_from_backend: n_spec missing id'); - - var n = __addNode(n_spec, false, false); - n_set.push(n); - }); - - data['link_set'].map(function(l_spec){ - var l_ptr = model_util.adapt_format_read_link_ptr(l_spec); - - util.assert(undefined != l_ptr.id, 'load_from_backend: l_ptr missing id'); - - // resolve link ptr - var src = find_node__by_id(l_ptr.__src_id), - dst = find_node__by_id(l_ptr.__dst_id); - - // cleanup & reuse as link_spec - delete l_ptr.__src_id; - delete l_ptr.__dst_id; - var link_spec = l_ptr; - var link = model_core.create_link_from_spec(src, dst, link_spec); - var l = addLink(link, false); - l_set.push(l); - }); - - undefined != on_success && on_success() - } - - rz_api_backend.clone(0, on_success__ajax); - } - this.load_from_backend = load_from_backend; - - this.load_from_json = function(json) { - var data = JSON.parse(json), - added_names, - that = this; - - clear(); - if (data == null) { - console.log('load callback: no data to load'); - return; - } - added_names = data.nodes.map(function(node) { - return that.__addNode({id:node.id, name:node.name ? node.name : node.id, - type:node.type,state:"perm", - start:new Date(node.start), - end:new Date(node.end), - status:node.status, - url:node.url, - x: node.x, - y: node.y, - }, false, false).name; - }); - data.links.forEach(function(link) { - that.addLink(link.__src, link.__dst, link.name, "perm"); - }); - this.clear_history(); - rz_bus.names.push(added_names); - } - - this.save_to_json = function() { - var d = {"nodes":[], "links":[]}; - for(var i = 0 ; i < nodes.length ; i++){ - var node = nodes[i]; - d['nodes'].push({ - "id": node.id, - "name": node.name, - "type":node.type, - "state":"perm", - "start":node.start, - "end":node.end, - "status": node.status, - "url": node.url, - "x": node.x, - "y": node.y, - }); - } - for(var j=0 ; j < links.length ; j++){ - var link = links[j]; - d['links'].push({ - "__src":link.__src.id, - "__dst":link.__dst.id, - "name":link.name - }); - } - return JSON.stringify(d); - } - - this.set_user = function(user) { - var elem = $('svg g.zoom')[0]; - this.user = user; - this.history = new history.History(this.user, this, elem); - } - - function clear_history() { - if (this.history !== undefined) { - this.history.clear(); - } - } - - this.clear_history = clear_history; - - var get_nodes = function() { - return nodes; - }; - this.nodes = get_nodes; - - var get_links = function() { return links; }; - this.links = get_links; - - function setRegularState() { - var x, node, link, s; - - for (x in nodes) { - node = nodes[x]; - s = node.state; - if (s === 'chosen' || s === 'enter' || s === 'exit') { - node.state = 'perm'; - } - } - for (x in links) { - link = links[x]; - s = link.state; - if (s === 'chosen' || s === 'enter' || s === 'exit') { - link.state = 'perm'; - } - } - } - this.setRegularState = setRegularState; - - this.findByVisitors = function(node_visitor, link_visitor) { - var n_length = nodes.length, - l_length = links.length, - selected = [], - i, - node, - link, - state; - - if (!node_visitor) { - return; - } - - for (i = 0 ; i < n_length; ++i) { - node = nodes[i]; - if (node.state == 'temp') { - continue; - } - if (node_visitor(node)) { - selected.push(node); - } - } - return selected; - } - - function markRelated(names) { - removeRelated(); - nodes.forEach(function (node) { - names.forEach(function (name) { - if (compareNames(node.name, name) && node.state != 'temp') { - node.state = 'related'; - } - }); - }); - } - this.markRelated = markRelated; - - function removeRelated() { - nodes.forEach(function (node) { - if (node.state == 'related') { - node.state = 'perm'; - } - }); - } - this.removeRelated = removeRelated; - -} - -return { - Graph: Graph, -}; - -}); diff --git a/src/model/util.js b/src/model/util.js deleted file mode 100644 index d938fbc9..00000000 --- a/src/model/util.js +++ /dev/null @@ -1,134 +0,0 @@ -"use strict" - -/** - * model utility functions: - convert from/to client/backend data - * representations - */ -define([ 'jquery', 'model/diff' ], function($, model_diff) { - - function __sanitize_label__write(label_str){ - var ret = label_str[0].toUpperCase() + - label_str.substring(1).toLowerCase(); - return ret; - } - - function __sanitize_label__read(label_str){ - return label_str.toLowerCase(); - } - - /** - * read by adapting from backend format - */ - function adapt_format_read_node(n_raw) { - var ret; - - ret = $.extend({ - // type: - // - discard all but first label - // - adjust to lowercase - 'type' : __sanitize_label__read(n_raw['__label_set'][0]), - 'state' : 'perm', - }, n_raw); - - delete ret.__label_set; - - return ret; - } - - /** - * write by adapting to backend format - */ - function adapt_format_write_node(n_raw) { - var ret = $.extend({ - }, n_raw); - - ret['__label_set'] = [__sanitize_label__write(n_raw.type)]; - - delete ret.state; - delete ret.status - delete ret.type; - - return ret - } - - /** - * read by adapting from backend format - */ - function adapt_format_read_link_ptr(l_raw) { - var ret; - - ret = $.extend({ - '__src_id' : l_raw['__src_id'], - '__dst_id' : l_raw['__dst_id'], - // type: - // - discard all but first label - // - adjust to lowercase - '__type' : __sanitize_label__read(l_raw['__label_set'][0]), - 'state' : 'perm', - }, l_raw); - - ret['name'] = ret['__type']; - - delete ret.__label_set; - - return ret; - } - - /** - * write by adapting to backend format - */ - function adapt_format_write_link(l_raw) { - var ret = $.extend({ - '__src_id' : l_raw.source.id, - '__dst_id' : l_raw.target.id, - }, l_raw); - - ret['__label_set'] = [__sanitize_label__write(l_raw.__type)]; - - delete ret.__dst; - delete ret.__src; - delete ret.source; // introduced by d3 accessor methods - delete ret.state; - delete ret.status; - delete ret.target; - - return ret; - } - - /** - * write adapt diff from node set, link set. sets may be passed by reference - * as they are cloned - */ - function adapt_format_write_topo_diff(n_set, l_set) { - - var new_n_set = $.extend([], n_set); - var new_l_set = $.extend([], l_set); - - // filter out 'bubble' nodes - new_n_set = new_n_set.filter(function(n) { - return 'bubble' != n.type; - }); - - new_n_set = $.map(new_n_set, function(n, _) { - return adapt_format_write_node(n); - }) - - new_l_set = $.map(new_l_set, function(l, _) { - return adapt_format_write_link(l); - }) - - var topo_diff = new model_diff.new_topo_diff({ - node_set_add : new_n_set, - link_set_add : new_l_set - }); - return topo_diff; - } - - return { - adapt_format_read_node : adapt_format_read_node, - adapt_format_read_link_ptr : adapt_format_read_link_ptr, - adapt_format_write_node : adapt_format_write_node, - adapt_format_write_link : adapt_format_write_link, - adapt_format_write_topo_diff : adapt_format_write_topo_diff, - } -}); \ No newline at end of file diff --git a/src/robot.js b/src/robot.js deleted file mode 100644 index 700e41b6..00000000 --- a/src/robot.js +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index da073f9f..00000000 --- a/src/rz_api_backend.js +++ /dev/null @@ -1,222 +0,0 @@ -"use strict"; - -/** - * API calls designed to execute against a local backend service - */ -define(['rz_config'], function(rz_config) { - - function RZ_API_Backend() { - - var rz_server_url = 'http://' + rz_config.rz_server_host + ':' + rz_config.rz_server_port; - - /** - * issue rhizi server ajax call - */ - var ajax_rs = function(path, req_opts, on_success, on_error) { - - function on_error_wrapper(xhr, err_text, err_thrown) { - // log wrap callback - console.error('error: \'' + err_text + '\''); - if (on_error && typeof (on_error) === "function") { - on_error(err_type, err_text); - } - } - - function on_success_wrapper(xhr, text) { - // log wrap callback - var ret_data = xhr.data; - console.log('success: ' + JSON.stringify(ret_data)); - - if (on_success) { - on_success(ret_data); - } - } - - /* - * add common request options - */ - req_opts.dataType = "json"; - req_opts.contentType = "application/json; charset=utf-8"; - req_opts.error = on_error_wrapper; - req_opts.success = on_success_wrapper; - req_opts.headers = {}; - req_opts.timeout = 8000; // ms - req_opts.crossDomain = true; - - $.ajax(rz_server_url + path, req_opts); - } - - /** - * common attr_diff - */ - this.commit_diff__attr = function(attr_diff, on_success, on_error) { - - var post_dict = { - 'attr_diff' : attr_diff - } - - var req_opts = { - type : 'POST', - data : JSON.stringify(post_dict), - }; - - return ajax_rs('/graph/diff-commit-attr', req_opts, on_success, - on_error); - } - - /** - * commit topo_diff - */ - this.commit_diff__topo = function(topo_diff, on_success, on_error) { - - var post_dict = { - 'topo_diff' : topo_diff - } - - var req_opts = { - type : 'POST', - data : JSON.stringify(post_dict), - }; - - return ajax_rs('/graph/diff-commit-topo', req_opts, on_success, - on_error); - } - - /** - * commit vis_diff - */ - this.commit_diff__vis = function(vis_diff, on_success, on_error) { - // TODO impl - } - - /** - * commit a diff_set - */ - this.commit_diff__set = function(diff_set, on_success, on_error) { - - var post_dict = { - 'diff_set' : diff_set - } - - var req_opts = { - type : 'POST', - data : JSON.stringify(post_dict), - }; - - return ajax_rs('/graph/diff-commit-set', req_opts, on_success, - on_error); - } - - /** - * clone rhizi repo - */ - this.clone = function(depth, on_success, on_error) { - - // prep request - var req_opts = { - type : 'POST', - }; - - ajax_rs('/graph/clone', req_opts, on_success, on_error); - } - - /** - * load node-set by id attribute - * - * @param on_complete_cb - * will be called with the returned json data on successful - * invocation - * @param on_error - * error callback - */ - this.load_node_set = function(id_set, on_success, on_error) { - - // prep request data - var post_dict = { - 'id_set' : id_set - } - - // prep request - var req_opts = { - type : 'POST', - data : JSON.stringify(post_dict), - }; - - return ajax_rs('/load/node-set-by-id', req_opts, on_success, - on_error); - } - - /** - * load link set by src / dst id - */ - this.load_link_set = function(link_ptr_set, on_success, on_error) { - - // prep request data - var post_dict = { - 'link_ptr_set' : link_ptr_set - } - - // prep request - var req_opts = { - type : 'POST', - data : JSON.stringify(post_dict), - }; - - return ajax_rs('/load/link-set/by_link_ptr_set', req_opts, - on_success, on_error); - } - - /** - * add a node set - */ - this.add_node_set = function(n_set, on_success, on_error) { - var topo_diff = new Topo_Diff(); - topo_diff.node_set_add = n_set; - - return this.topo_diff_commit(topo_diff, on_success, on_error); - } - - /** - * add a link set - */ - this.add_link_set = function(l_set, on_success, on_error) { - var topo_diff = new Topo_Diff(); - topo_diff.link_set_add = l_set; - - return this.topo_diff_commit(topo_diff); - } - - /** - * remove node set - */ - this.remove_node_set = function() { - var topo_diff = null; - return this.topo_diff_commit(topo_diff); - } - - /** - * remove link set - */ - this.remove_link_set = function() { - var topo_diff = null; - return this.topo_diff_commit(topo_diff); - } - - /** - * update node set - */ - this.update_node_set = function(attr_diff, on_success, on_error) { - return this.attr_diff_commit(attr_diff, on_success, on_error); - } - - /** - * update link set - */ - this.update_link_set = function() { - var attr_diff = null; - return this.attr_diff_commit(null); - } - } - - return new RZ_API_Backend(); -}); diff --git a/src/rz_api_mesh.js b/src/rz_api_mesh.js deleted file mode 100644 index 9014dbac..00000000 --- a/src/rz_api_mesh.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * 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_bus.js b/src/rz_bus.js deleted file mode 100644 index 38033bc4..00000000 --- a/src/rz_bus.js +++ /dev/null @@ -1,15 +0,0 @@ -"use strict" - -define(['consts', 'Bacon'], -function(consts, Bacon) -{ - var ui_key_bus = new Bacon.Bus(), - ui_input_bus = new Bacon.Bus(), - names_bus = new Bacon.Bus(); - - return { - ui_key: ui_key_bus, - ui_input: ui_input_bus, - names: names_bus, - }; -}); diff --git a/src/rz_config.js b/src/rz_config.js deleted file mode 100644 index e55e74d0..00000000 --- a/src/rz_config.js +++ /dev/null @@ -1,9 +0,0 @@ -define(function() { - - return { - 'rand_id_generator' : 'hash', - 'rz_server_host': 'rhizi.local', - 'rz_server_port': '8080', - 'backend_enabled': false, - }; -}); diff --git a/src/rz_core.js b/src/rz_core.js deleted file mode 100644 index 7ec19692..00000000 --- a/src/rz_core.js +++ /dev/null @@ -1,756 +0,0 @@ -"use strict" - -define(['jquery', 'd3', 'consts', 'rz_bus', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer', 'view/selection', 'rz_config'], -function($, d3, consts, rz_bus, util, model_graph, model_core, view_helpers, view, rz_observer, selection, rz_config) { - -var addednodes = [], - vis, - graphinterval = 0, - timeline_timer = 0, - deliverables = [], - circle, // <-- should not be module globals. - scrollValue = 0, - graph, - drag, - force; - -// "CSS" for SVG elements. Reused for editing elements. -var node_text_dx = 15, - node_text_dy = '.30em', - svg_input_fo_node_x = node_text_dx, - svg_input_fo_node_y = '-.70em', - svg_input_fo_height = '30px'; - -/** - * svgInput - creates an embedded input element under a given - * - * edit_node(@sibling, @node) - * edit_link(@sibling, @link) - */ -var svgInput = (function() { - var measure_node = $('#measure-node')[0], - measure_link = $('#measure-link')[0], - original_element, - is_link; - - function appendForeignElementInputWithID(base, elemid, width, height) - { - var input = document.createElement('input'), - body = document.createElement('body'), - fo = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); - - body.appendChild(input); - - fo.setAttribute('height', height || svg_input_fo_height); - fo.style.pointerEvents = 'none'; - input.style.pointerEvents = 'all'; - fo.appendChild(body); - base.appendChild(fo); - input.setAttribute('id', elemid); - return input; - } - - function measure(text) - { - var span; - - span = is_link ? measure_link : measure_node; - span.innerHTML = text; - return span.getBoundingClientRect().width; // $().width() works too - } - - function onkeydown(e) { - var ret = undefined, - jelement = createOrGetSvgInput(), - element = jelement[0], - newname = jelement.val(), - fo = createOrGetSvgInputFO(), - d; - - if (element != this) { - console.log('unexpected editname_on_keypress this should be the svg-input element'); - } - - if (e.which == 13 || e.which == 27) { - ret = false; - d = jelement.data().d; - if (e.which == 13 && newname != d.name) { - if (d.hasOwnProperty('__src')) { - graph.editLink(d.__src.id, d.__dst.id, newname); - } else { - graph.editName(d.id, newname); - } - rz_bus.names.push([newname]); - update_view__graph(true); - } - hide(); - } - rz_bus.ui_key.push({where: consts.KEYSTROKE_WHERE_EDIT_NODE, keys: [e.which]}); - return ret; - }; - - function resize_measure(e) { - resize(measure($(e.target).val()) + 30); - } - - function resize(new_width) { - var svg_input = createOrGetSvgInput(), - fo = createOrGetSvgInputFO(); - - svg_input.css('width', new_width); - fo.attr('width', new_width); - } - - // FIXME: element being deleted. Some delete is legit - removal of related element. Some isn't (a click). - // Instead of investigating (time constraint) reparenting as sibling, and introducing - // this function. Cost of creation of element is negligble, it's just ugly.. - function createOrGetSvgInput() - { - var svg_input_name = 'svg-input', - svg_input_selector = '#' + svg_input_name, - svg_input = $(svg_input_selector); - - if (svg_input.length == 0) { - console.log('creating new svg-input'); - svg_input = $(appendForeignElementInputWithID(vis[0][0], svg_input_name)); - svg_input.on('keydown', onkeydown); - svg_input.bind('change keypress', resize_measure); - } - return svg_input; - } - - function createOrGetSvgInputFO() - { - return createOrGetSvgInput().parent().parent(); - } - - /* - * @param e visual node element - * @param n node model object - */ - function enable(e, n) { - var oldname = n.name, - svg_input = createOrGetSvgInput(), - fo = createOrGetSvgInputFO(); - - is_link = n.hasOwnProperty('__src'); - - e.parentNode.appendChild(fo[0]); // This will unparent from the old parent - if (is_link) { - fo.attr('transform', e.getAttribute('transform')); - // XXX links set the text-anchor middle attribute. no idea how to do that - fo.attr('x', -$(e).width() / 2); - fo.attr('y', -$(e).height() / 2 - 3); // XXX This minus 3 is only kinda ok. - fo.attr('class', 'svg-input-fo-link'); - } else { - fo.attr('x', svg_input_fo_node_x); - fo.attr('y', svg_input_fo_node_y); - fo.attr('transform', null); - fo.attr('class', 'svg-input-fo-node'); - } - // Set width correctly - resize(measure(oldname) + 30); - fo.show(); - svg_input.val(oldname); - svg_input.data().d = n; - svg_input.focus(); - if (original_element) { - original_element.show(); - } - original_element = $(e); - original_element.hide(); - // TODO: set cursor to correct location in text - } - - function hide() { - createOrGetSvgInputFO().hide(); - if (original_element && original_element.show) { - original_element.show(); - } - } - - return { - enable: enable, - hide: hide, - }; -}()); - - - -function recenterZoom() { - vis.attr("transform", "translate(0,0)scale(1)"); -} - -// zoom or drag -var zoomInProgress = false; - -var initDrawingArea = function () { - - function zoom() { - zoomInProgress = true; - vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); - d3.event.sourceEvent.stopPropagation(); - } - - function dragstarted(d) { - d3.event.sourceEvent.stopPropagation(); - d3.select(this).classed("dragging", true); - d.dragstart = {clientX:d3.event.sourceEvent.clientX, clientY:d3.event.sourceEvent.clientY}; - force.stop(); - } - - function dragged(d) { - d.x = d3.event.x; - d.y = d3.event.y; - tick(); - } - - function dragended(d) { - d3.select(this).classed("dragging", false); - d3.select(this).classed("fixed", true); // TODO: this is broken since we override all the classes. Need to switch to class addition/removal (i.e. use classed for everything) or set class in one location (so here just set a value on the node, not the element) - if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 || - d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) { - tick(); - force.resume(); - } - } - - graph = new model_graph.Graph(); - - var user_id = $('#user_id'), - user = user_id.text(); - - if (user_id.length > 0) { - console.log('found user ID: \'' + user + '\''); - graph.set_user(user); - } - - var el = document.body; - vis = d3.select(el).append("svg:svg") - .attr('id', 'canvas_d3') - .attr("width", '100%') - .attr("height", '100%') - .attr("pointer-events", "all") - .append("g") - .attr("class", "zoom"); - - d3.select(el).select("svg").append("svg:defs") - .data(["end"]) // Different link/path types can be defined here - .append("svg:marker") // This section adds in the arrows - .attr("id", String) - .attr("viewBox", "0 -5 10 10") - .attr("refX", 22) - .attr("refY", -1.5) - .attr("markerWidth", 4) - .attr("markerHeight", 4) - .attr("orient", "auto") - .append("svg:path") - .attr("d", "M0,-5L10,0L0,5"); - - /* - * init zoom behavior - */ - var zoom_obj = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); - zoom_obj(d3.select('#canvas_d3')) - d3.select("svg").on("dblclick.zoom", null); // disable zoom on double click - - $('svg').click(svg_click_handler); - - // SVG rendering order is last rendered on top, so to make sure - // all links are below the nodes we group them under a single g - vis.append("g").attr("id", "link-group"); - vis.append("g").attr("id", "selected-link-group"); - - drag = d3.behavior.drag() - .origin(function(d) { return d; }) - .on("dragstart", dragstarted) - .on("drag", dragged) - .on("dragend", dragended); - - // $('#canvas_d3').dblclick(canvas_handler_dblclick); - see #138 - if (rz_config.backend_enabled){ - graph.load_from_backend( function(){ - update_view__graph(false); - }); - } -} - -function init_force_layout(){ - var el = document.body; - var w = $(el).innerWidth(), - h = $(el).innerHeight(); - - force = d3.layout.force() - .distance(120) - .gravity(0.12) - .charge(-1800) - .size([w, h]) - .on("tick", tick) - .start(); -} - -initDrawingArea(); -init_force_layout(); - -/** - * find the visual element counterpart of a given model object. This relies on - * the visual element having an id attribute equal to the object's id. - * - * @return null if visual element is not found - */ -function locate_visual_element(model_obj){ - var id_sel = $('#' + model_obj.id); - if (0 == id_sel.length){ - console.warn('unable to find visual element for model object: object id: ' + model_obj.id.toString()) - return null; - } - return id_sel[0]; -} - -/** - * add node on canvas double click - */ -function canvas_handler_dblclick(){ - var n = model_core.create_node__set_random_id(); - n.name = ''; // will be set by user - - graph.addNode(n); - update_view__graph(); - - var n_ve = locate_visual_element(n); // locate visual element - - var on_slowdown_cb = function(){ - svgInput.enable($(n_ve).find('.nodetext'), n); - observer.disconnect(); - } - var mutation_handler = rz_observer.new_Mutation_Handler__on_dxy_slowdown(on_slowdown_cb); - var observer = rz_observer.new_MutationObserver(mutation_handler); - mutation_handler.on_slowdown_threshold_reached; - - observer.observe(n_ve, { - subtree: false, - childList : false, - attributes: true, - attributeOldValue : true, - }); -} - -/** - * update view: graph - */ -function update_view__graph(no_relayout) { - var node, - link, - link_g, - linktext, - nodetext, - unselected_link_group = document.querySelector('#link-group'), - selected_link_group = document.querySelector('#selected-link-group'); - - link = vis.selectAll("g.link") - .data(graph.links(), function(d) { return d.id; }); - - link_g = link.enter().append('g') - .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping - .attr('class', 'link graph') - - link_g.append("path") - .attr("class", function(d) { - return d.state + ' link graph'; - }) - .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping - .attr("marker-end", "url(#end)"); - - // second path for larger click area - link_g.append("path") - .attr("class", "ghostlink") - .on("click", function(d, i) { - if (zoomInProgress) { - // don't disable zoomInProgress, it will be disabled by the svg_click_handler - // after this events bubbles to the svg element - return; - } - var that = this, - src = this.link.__src, - dst = this.link.__dst; - - view.edge_info.on_delete(function () { - graph.removeLink(that.link); - update_view__graph(true); - view.edge_info.hide(); - }); - view.edge_info.show(d); - selection.update([src, dst]); - update_view__graph(true); - }); - - link.attr("class", function(d, i){ - var temp_and = (d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp") ? "temp_and" : ""; - - return ["graph link", temp_and, selection.selected_class(d)].join(' '); - }); - - link.selectAll('path.link') - .attr('class', function(d) { - return [d.state, selection.selected_class(d), "link graph"].join(' '); - }); - - link.exit().remove(); - - vis.selectAll('.ghostlink') - .data(graph.links()) - .each(function (d) { - this.link = d; - }); - - linktext = vis.selectAll(".linklabel") - .data(graph.links(), function(d) { return d.id; }); - linktext.enter() - .append("text") - .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping - .attr("class", function(d) { - return ["linklabel graph", selection.selected_class(d)].join(' '); - }) - .attr("text-anchor", "middle") - .on("click", function(d, i) { - if (d.state !== "temp") { - svgInput.enable(this, d); - } - }); - - linktext - .text(function(d) { - var name = d.name || ""; - if (!(d.__dst.state === "temp" || - d.__src.state === "chosen" || d.__dst.state === "chosen")) { - return ""; - } - if (name.length < 25 || d.__src.state === "chosen" || - d.__dst.state === "chosen" || d.state==="temp") { - return name; - } else { - return name.substring(0, 14) + "..."; - } - }); - - linktext.exit().remove(); - - node = vis.selectAll(".node") - .data(graph.nodes(), function(d) { - return d.id; - }); - - var nodeEnter = node.enter() - .append("g") - .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping - .attr('visibility', 'hidden') // made visible on first tick - .call(drag); - - // reorder nodes so selected are last, and so rendered last, and so on top. - (function () { - var ontop = [], - bubble; - - node.each(function (d) { - this.node = d; - }) - .attr('class', function(d) { - if (selection.node_selected(d)) { - if (d.type == 'bubble') { - bubble = this; - } else { - ontop.push(this); - } - } - return ['node', selection.selected_class(d)].join(' '); - }); - if (bubble === undefined) { - // nothing to do if there is no bubble - return; - } - function reparent(new_parent, element) { - if (element.parentNode == new_parent) { - return; - } - new_parent.appendChild(element); - } - // move link to correct group - // O(|links|*|ontop|) - link.each(function (d) { - if (ontop.some(function (node) { - var d_node = node.node; - return d.__src == d_node || d.__dst == d_node; - })) - { - reparent(selected_link_group, this); - } else { - reparent(unselected_link_group, this); - } - }); - linktext.each(function (d) { - if (selection.node_selected(d)) { - ontop.push(this); - } - }); - function moveToEnd(e) { - e.parentNode.appendChild(e); - } - moveToEnd(bubble); - ontop.reverse().forEach(function (e) { - moveToEnd(e); - }); - var count_links = function() { - return selected_link_group.childElementCount + unselected_link_group.childElementCount; - }; - // put back on top link group on top - selected_link_group.parentNode.insertBefore(selected_link_group, bubble.nextSibling); - })(); - - nodetext = nodeEnter.insert("text") - .attr("class", "nodetext graph") - .attr("dx", node_text_dx) - .attr("dy", node_text_dy) - .on("click", function(d, i) { - if (d3.event.defaultPrevented) { - // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on - return; - } - if (d.state !== "temp") { - svgInput.enable(this, d); - selection.update([d]); - showNodeInfo(this.parentNode.node, i); - } - d3.event.stopPropagation(); - }); - - node.select('g.node text') - .text(function(d) { - if (!d.name) { - return d.type == 'bubble' ? "" : "_"; - } - if (d.state === "temp" || d.state === 'chosen' - || d.state === "enter" || d.state === "exit") { - return d.name; - } else { - if (d.name.length < 28) { - return d.name; - } else { - return d.name.substring(0, 25) + "..."; - } - } - }); - - circle = nodeEnter.insert("circle"); - node.select('g.node circle') - .attr("class", function(d) { - return d.type + " " + d.state + " circle graph"; - }) - .attr("r", function(d) { - return view_helpers.customSize(d.type) - 2; - }) - .on("click", function(d, i) { - if (d3.event.defaultPrevented) { - // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on - return; - } - d3.event.stopPropagation(); - selection.update([d]); - if(d.state !== "temp") { - showNodeInfo(d, i); - } - }); - circle.append("svg:image") - .attr("class", "status graph") - .attr('x', -7) - .attr('y', -8) - .attr('width', 15) - .attr('height', 15) - .attr("xlink:href", function(d) { - switch (d.status) { - case "done": - return "res/img/check.png"; - break; - case "current": - return "res/img/wait.png"; - break; - case "waiting": - return "res/img/cross.png"; - break; - } - }); - - node.exit().remove(); - - //update deliverables - deliverables = []; - var nodes = graph.nodes(); - for (var i = 0; i < nodes.length; i++) { - var current = nodes[i]; - if (current.type === "third-internship-proposal") { - deliverables.push({ - "id": nodes[i].id, - "startdate": nodes[i].start, - "enddate": nodes[i].end - }); - } - //Do something - } - - force.nodes(graph.nodes()) - .links(graph.links()) - - if (no_relayout) { - // XXX If we are stopped we need to update the text of the links at least, - // and this is the simplest way - tick(); - } else { - force.alpha(0.1).start(); - } -} - - - -var debug_print = function(message) { - var element = $(".debug"); - if (element.length == 1) { - element.html(message); - } else { - console.log(message); - } -} - -function check_for_nan(x) { - if (Number.isNaN(x)) { - console.log('nan problem'); - force.stop(); - } - return Number.isNaN(x); -} - -var newnodes=1; -function tick(e) { - //console.log(e); - //$(".debug").html(force.alpha()); - var node = vis.selectAll(".node") - .data(force.nodes(), function(d) { - return d.id; - }); - var link = vis.selectAll("path.link") - .data(graph.links(), function(d) { - return d.id; - }); - var linktext = vis.selectAll(".linklabel").data(graph.links()); - - function transform(d) { - if (check_for_nan(d.x) || check_for_nan(d.y)) { - return; - } - return "translate(" + d.x + "," + d.y + ")"; - } - - //circles animation - var tempcounter = 0, - temptotal = graph.nodes().filter(function(d){ - return d.state === "temp" && d.type !== "chainlink" && d.type !== "bubble"; - }).length; - if (temptotal !== newnodes) { - newnodes += temptotal / 15 / (newnodes * newnodes); - } - newnodes = Math.max(1, Math.min(newnodes, temptotal)); - graph.nodes().forEach(function(d, i) { - var r, a; - if (d.state === "temp") { - tempcounter++; - if (d.type==="chainlink" || d.type==="bubble") { - d.x = window.innerWidth / 2; - d.y = window.innerHeight / 2; - } else { - r = 60 + newnodes * 20; - a = -Math.PI + Math.PI * 2 * (tempcounter-1) / newnodes + 0.3; - d.x = window.innerWidth / 2 + r * Math.cos(a); - d.y = window.innerHeight / 2 + r * Math.sin(a); - } - check_for_nan(d.x); - check_for_nan(d.y); - } - }); - - link.attr("d", function(d, i) { - var d_val, - ghost; - - var dx = d.__dst.x - d.__src.x, - dy = d.__dst.y - d.__src.y, - dr = Math.sqrt(dx * dx + dy * dy); - d_val = "M" + d.__src.x + "," + d.__src.y + "L" + d.__dst.x + "," + d.__dst.y; - // update ghostlink position - ghost = $(this.nextElementSibling); - ghost.attr("d", d_val); - return d_val; - }); - - linktext.attr("transform", function(d) { - return "translate(" + (d.__src.x + d.__dst.x) / 2 + "," + (d.__src.y + d.__dst.y) / 2 + ")"; - }); - - node.attr("transform", transform); - - // After initial placement we can make the nodes visible. - //links.attr('visibility', 'visible'); - node.attr('visibility', 'visible'); -} - -function showNodeInfo(d, i) { - view.node_info.on_save(function(e, form_data) { - - graph.update_node(d, form_data, function(){ - var old_type = d.type, - new_type = form_data.type; - - if (new_type != old_type) { - view.node_info.show(d); - } - - view.node_info.hide(); - update_view__graph(true); - }); - - return false; - }); - - view.node_info.on_delete(function() { - if (confirm('This node and all its connections will be deleted, are you sure?')) { - graph.removeNode(d.id); - update_view__graph(false); - view.node_info.hide(); - } - }); - - view.node_info.show(d); -} - -function svg_click_handler(e) { - if (zoomInProgress) { - zoomInProgress = false; - return; - } - if (e.originalEvent.target.nodeName != 'svg') { - return; - } - svgInput.hide(); - selection.clear(); - view.hide(); - update_view__graph(true); -} - -return { - graph: graph, - force: force, - load_from_json: function(result) { - graph.load_from_json(result); - recenterZoom(); - update_view__graph(false); - }, - update_view__graph : update_view__graph, -} -}); /* close define call */ diff --git a/src/rz_observer.js b/src/rz_observer.js deleted file mode 100644 index 718fd0a2..00000000 --- a/src/rz_observer.js +++ /dev/null @@ -1,114 +0,0 @@ -"use strict" - -/** - * DOM object observers - */ -define( -[], -function() { - -var MutationObserver = window.MutationObserver; -// more portable version: var MutationObserver = -// window.MutationObserver || -// window.WebKitMutationObserver; - -/** - * observe SVG object's translate attribute (x,y values), measure - * change rate & invoke on_slowdown_threshold_reached() upon - * reaching change slowdown threshold. - * - * Caller is responsible to disconnect observer. - */ -function Mutation_Handler__on_dxy_slowdown( - on_slowdown_threshold_reached) { - - var x_cur, y_cur, d, dx, dy, avg_d = 0; - - this.on_slowdown_threshold_reached = on_slowdown_threshold_reached; - - /** - * handle observer mutation - */ - this.handle_mutation = function(m) { - if ('transform' != m.attributeName || null == m.oldValue) { - return; - } - - /* - * parsed txt example: - * "translate(173.6007550157428,275.43228723527193)" - */ - var rgx_m = m.oldValue.match(/\((\d+.\d+),(\d+.\d+)\)/); - x_cur = rgx_m[1]; - y_cur = rgx_m[2]; - if (undefined == this.x_prv) { - this.x_prv = x_cur; - this.y_prv = y_cur; - return; - } - dx = x_cur - this.x_prv; - dy = y_cur - this.y_prv; - - var d = Math.sqrt(dx * dx + dy * dy); - // average across last samples - avg_d = (1.0 - this.most_recent_sample_weight) * avg_d - + this.most_recent_sample_weight * d; - - // debug - // console.log({ - // 'dx' : dx, - // 'dy' : dy, - // 'd' : d, - // 'avg_d' : avg_d, - // }); - - if (avg_d < this.slowdown_threshold) { - // slowdown threshold reached - if (this.on_slowdown_threshold_reached) { - this.on_slowdown_threshold_reached(); - } - } - - this.x_prv = x_cur; - this.y_prv = y_cur; - } -} - -function new_Mutation_Handler__on_dxy_slowdown( - on_slowdown_threshold_reached, slowdown_threshold, - most_recent_sample_weight) { - slowdown_threshold = slowdown_threshold || 0.07; - most_recent_sample_weight = most_recent_sample_weight || 0.3; - var ret = new Mutation_Handler__on_dxy_slowdown( - on_slowdown_threshold_reached); - ret.slowdown_threshold = slowdown_threshold; - ret.most_recent_sample_weight = most_recent_sample_weight; - return ret; -} - -function new_MutationObserver(handler) { - /* - * FIXME implemente with inheritence - currently triggers - * 'illegal invocation', possibly due to some interaction with - * requirejs - */ - // var o = new - // window.MutationObserver(Mutation_Observer.handler); - // var ret = Object.create(o); - // ret. = undefined; - // return ret; - var for_each_mutaion = function(m_set) { - m_set.forEach(function(m) { - handler.handle_mutation(m); - }) - }; - - return new MutationObserver(for_each_mutaion); -} - -return { - new_MutationObserver : new_MutationObserver, - new_Mutation_Handler__on_dxy_slowdown : new_Mutation_Handler__on_dxy_slowdown, -}; - -}); diff --git a/src/server-tests/neo4j_test_util.py b/src/server-tests/neo4j_test_util.py new file mode 100644 index 00000000..d5aa640a --- /dev/null +++ b/src/server-tests/neo4j_test_util.py @@ -0,0 +1,54 @@ +import uuid +import string +from random import choice +import db_controller as dbc + +def rand_id(): + return str(uuid.uuid4()) + +def rand_label(length=8): + """ + return random label + """ + char_set = string.ascii_lowercase + string.ascii_uppercase + string.digits + return ''.join([choice(string.ascii_lowercase)] + [choice(char_set) for _ in range(length - 1)]) + +def flush_db(db_ctl): + """ + complete DB flush: remove all nodes & links + """ + db_ctl.exec_cypher_query('match (n) optional match (n)-[r]-() delete n,r') + + +def gen_rand_data(db_ctl, lim_n=128, lim_r=256, prob_link_create = 0.3): + """ + generate random DB data + + @return: tuple consisting of the random node,link labels generated + """ + assert 2 <= lim_n + + n_label = rand_label() + r_label = rand_label() + q_arr = ['with 0 as _', # TODO clean: foreach triggers SyntaxException: otherwise + 'foreach (rid in range(0,%d)' % (lim_n - 1), + '|', + 'create (:%s {id:rid, n_attr_0:toInt(%d * rand())}))' % (n_label, lim_n) + ] + + q = ' '.join(q_arr) + op = dbc.DBO_cypher_query(q) + db_ctl.exec_op(op) + + q_arr = ['match (s:%s),(d:%s)' % (n_label, n_label), + 'with s,d', + 'limit %d' % (lim_r - 1), + 'where rand() < %.2f' % (prob_link_create), + 'create (s)-[:%s {l_attr_0:toInt(%d * rand())}]->(d)' % (r_label,lim_r)] + + q = ' '.join(q_arr) + op = dbc.DBO_cypher_query(q) + db_ctl.exec_op(op) + + return (n_label, r_label) + diff --git a/src/server-tests/test_db_controller.py b/src/server-tests/test_db_controller.py new file mode 100644 index 00000000..79be62d2 --- /dev/null +++ b/src/server-tests/test_db_controller.py @@ -0,0 +1,327 @@ +import unittest +import logging +import db_controller as dbc + +from rhizi_server import Config +from neo4j_test_util import rand_id +from neo4j_test_util import flush_db +from neo4j_test_util import gen_rand_data +from neo4j_util import Neo4JException + +from model.graph import Attr_Diff +from model.graph import Topo_Diff +from model.model import Link + +class TestDBController(unittest.TestCase): + + db_ctl = None + log = None + + n_map = { 'Skill': [{'name': 'Kung Fu', 'id': 'skill_00' }, + {'name': 'Judo', 'id': 'skill_01' } + ], + + 'Person': [{'name': 'Bob', 'id': 'person_00', 'age': 128 }, + {'name': 'Alice', 'id': 'person_01', 'age': 256 } + ] + } + + l_map = { 'Knows' : [Link.link_ptr('person_00', 'skill_00'), + Link.link_ptr('person_00', 'skill_01')] } + + @classmethod + def setUpClass(self): + cfg = Config.init_from_file('res/etc/rhizi-server.conf') + self.db_ctl = dbc.DB_Controller(cfg) + self.log = logging.getLogger('rhizi') + self.log.addHandler(logging.StreamHandler()) + + # TODO rm when implemented: neo4j_test_util + self.db_ctl.exec_cypher_query('create index on :Person(id)') + self.db_ctl.exec_cypher_query('create index on :Skill(id)') + + def setUp(self): + flush_db(self.db_ctl) # remove once embedded DB test mode is supported + self.db_ctl.exec_op(dbc.DBO_add_node_set(self.n_map)) + self.db_ctl.exec_op(dbc.DBO_add_link_set(self.l_map)) + + def test_db_op_statement_iteration(self): + s_arr = ['create (b:Book {title: \'foo\'}) return b', + 'match (n) return n', ] + + op = dbc.DB_op() + op.add_statement(s_arr[0]) + op.add_statement(s_arr[1]) + + i = 0 + for _, s, r in op: + # access: second tuple item -> REST-form 'statement' key + self.assertEqual(s_arr[i], s['statement']) + self.assertEqual(None, r) + i = i + 1 + + self.db_ctl.exec_op(op) + + i = 0 + for _, s, r_set in op: + # access: second tuple item -> REST-form 'statement' key + self.assertNotEqual(None, r_set) + for x in r_set: + pass + i = i + 1 + + def test_add_node_set(self): + n_map = { 'T_test_add_node_set': [{'id': rand_id()}, {'id': rand_id()}] } + op = dbc.DBO_add_node_set(n_map) + + self.assertEqual(len(op.statement_set), 1) # assert a single statement is issued + + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 2) + + def test_add_link_set(self): + src_id = rand_id() + dst_id_0 = rand_id() + dst_id_1 = rand_id() + n_map = { 'T_test_add_node_set': [{'id': src_id }, + {'id': dst_id_0 }, + {'id': dst_id_1 }] } + self.db_ctl.exec_op(dbc.DBO_add_node_set(n_map)) + + l_map = { 'T_test_add_link_set' : [{'__src': src_id, '__dst': dst_id_0}, + {'__src': src_id, '__dst': dst_id_1}] } + + op = dbc.DBO_add_link_set(l_map) + self.assertEqual(len(op.statement_set), 2) # no support yet for parameterized statements for link creation + + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 2) + + def test_match_node_set_by_type(self): + op = dbc.DBO_match_node_id_set(filter_label='Person') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 2) + + op = dbc.DBO_match_node_id_set(filter_label='Nan_Type') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_match_node_set_by_attribute(self): + fam = { 'name': ['Bob', u'Judo'], 'age': [128] } + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_id_set(filter_attr_map=fam)) + self.assertEqual(len(n_set), 1) + + fam = { 'age': [128, 256, 404] } + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_id_set(filter_attr_map=fam)) + self.assertEqual(len(n_set), 2) + + def test_match_node_set_by_DB_id(self): + pass # TODO + + def test_match_node_set_by_id_attribute(self): + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_set_by_id_attribute(['skill_00', 'person_01'])) + self.assertEqual(len(n_set), 2) + + def test_match_link_set_by_type(self): + op = dbc.DBO_match_link_id_set(filter_label='Knows') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 2) + + op = dbc.DBO_match_link_id_set(filter_label='Nan_Type') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_load_link_set(self): + + # load by l_ptr + l_ptr = Link.link_ptr(src_id='person_00', dst_id='skill_00') + op = dbc.DBO_load_link_set.init_from_link_ptr(l_ptr) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 1) + + l_ptr = Link.link_ptr(src_id='person_00') + op = dbc.DBO_load_link_set.init_from_link_ptr(l_ptr) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 2) + + l_ptr = Link.link_ptr(dst_id='skill_00') + op = dbc.DBO_load_link_set.init_from_link_ptr(l_ptr) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 1) + + # load by l_ptr sets + l_ptr_set = [Link.link_ptr(s, d) for (s, d) in [('person_00', 'skill_00'), ('person_00', 'skill_01')]] + op = dbc.DBO_load_link_set.init_from_link_ptr_set(l_ptr_set) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 2) + + # this should return the same link twice + l_ptr_set = [Link.link_ptr(s, d) for (s, d) in [('person_00', 'skill_00'), ('person_00', 'skill_01')]] + l_ptr_set.append(Link.link_ptr(dst_id='skill_00')) + op = dbc.DBO_load_link_set.init_from_link_ptr_set(l_ptr_set) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 3) + + def test_load_node_set_by_DB_id(self): + """ + test node DB id life cycle + """ + + # create nodes, get DB ids + op = dbc.DBO_add_node_set({'T_test_load_node_set_by_DB_id': [{'name': 'John Doe'}, + {'name': 'John Doe'}]}) + id_set = self.db_ctl.exec_op(op) + + # match against DB ids + op = dbc.DBO_load_node_set_by_DB_id(id_set) + n_set = self.db_ctl.exec_op(op) + self.assertEqual(len(n_set), len(id_set), 'incorrect result size') + + def test_partial_query_set_execution_success(self): + """ + test: + - statement execution stops at first invalid statement + - assert create statement with result data does not actually persist in DB + + From the REST API doc: 'If any errors occur while executing statements, + the server will roll back the transaction.' + """ + n_id = 'test_partial_query_set_execution_success' + + op = dbc.DB_op() + op.add_statement("create (n:Person {id: '%s'}) return n" % (n_id), {}) # valid statement + op.add_statement("match (n) return n", {}) # valid statement + op.add_statement("non-valid statement #1", {}) + op.add_statement("non-valid statement #2", {}) + + self.assertRaises(Neo4JException, self.db_ctl.exec_op, op) + + self.assertEqual(len(op.result_set), 2) + self.assertEqual(len(op.error_set), 1) + + # assert node creation did not persist + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_set_by_id_attribute([n_id])) + self.assertEqual(len(n_set), 0) + + def test_topo_diff_commit(self): + n_0_id = rand_id() + n_1_id = rand_id() + n_2_id = rand_id() + n_T = 'T_test_topo_diff_commit' + + n_set = [{'__type': n_T, 'id': n_0_id }, + {'__type': n_T, 'id': n_1_id }, + {'__type': n_T, 'id': n_2_id }] + l_set = [{'__type': n_T, '__src_id': n_0_id, '__dst_id': n_1_id}, + {'__type': n_T, '__src_id': n_1_id, '__dst_id': n_0_id}] + + topo_diff = Topo_Diff(node_set_add=n_set, + link_set_add=l_set) + + op = dbc.DBO_topo_diff_commit(topo_diff) + op_ret = self.db_ctl.exec_op(op) + self.assertEqual(len(op_ret), 2) # to id-sets, nodes & links + self.assertEqual(len(op_ret[0]), 3) # expect id-set of length 3 + self.assertEqual(len(op_ret[1]), 2) # expect id-set of length 2 + + id_set = self.db_ctl.exec_op(dbc.DBO_match_node_set_by_id_attribute([n_0_id, n_1_id])) + self.assertEqual(len(id_set), 2) + + l_ptr = Link.link_ptr(src_id=n_0_id, dst_id=n_1_id) + id_set = self.db_ctl.exec_op(dbc.DBO_load_link_set.init_from_link_ptr(l_ptr)) + self.assertEqual(len(id_set), 1) + + l_ptr = Link.link_ptr(src_id=n_1_id, dst_id=n_0_id) + id_set = self.db_ctl.exec_op(dbc.DBO_load_link_set.init_from_link_ptr(l_ptr)) + self.assertEqual(len(id_set), 1) + + id_set_rm = [n_2_id] + topo_diff = Topo_Diff(node_set_rm=id_set_rm) + op = dbc.DBO_topo_diff_commit(topo_diff) + self.db_ctl.exec_op(op) + op = dbc.DBO_match_node_set_by_id_attribute(id_set_rm) + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_attr_diff_commit(self): + # create test node + n_id = rand_id() + topo_diff = Topo_Diff(node_set_add=[{'__type': 'T_test_attr_diff_commit', 'id': n_id, 'attr_rm': 0}]) + op = dbc.DBO_topo_diff_commit(topo_diff) + self.db_ctl.exec_op(op) + + # apply attr_diff + attr_diff = Attr_Diff() + attr_diff.add_node_attr_write(n_id, 'attr_0', 0) + attr_diff.add_node_attr_write(n_id, 'attr_1', 'a') + attr_diff.add_node_attr_rm(n_id, 'attr_rm') + + op = dbc.DBO_attr_diff_commit(attr_diff) + n_map = self.db_ctl.exec_op(op) + self.assertEqual(len(n_map), 1) + n = n_map.get(n_id) + self.assertTrue(None != n) + self.assertTrue(None == n.get('attr_rm')) + self.assertEqual(0, n.get('attr_0')) + self.assertEqual('a', n.get('attr_1')) + + # attr-set only + attr_diff = Attr_Diff() + attr_diff.add_node_attr_write(n_id, 'attr_2', 0) + + op = dbc.DBO_attr_diff_commit(attr_diff) + n_map = self.db_ctl.exec_op(op) + + # attr-remove only + attr_diff = Attr_Diff() + attr_diff.add_node_attr_rm(n_id, 'attr_2') + + op = dbc.DBO_attr_diff_commit(attr_diff) + n_map = self.db_ctl.exec_op(op) + + def test_rm_node_set(self): + n_0_id = rand_id() + n_1_id = rand_id() + n_2_id = rand_id() + n_3_id = rand_id() + n_T = 'T_test_rm_node_set' + + n_set = [{'__type': n_T, 'id': n_0_id }, + {'__type': n_T, 'id': n_1_id }, + {'__type': n_T, 'id': n_2_id }, + {'__type': n_T, 'id': n_3_id }] + l_set = [{'__type': n_T, '__src_id': n_2_id, '__dst_id': n_2_id}, + {'__type': n_T, '__src_id': n_2_id, '__dst_id': n_3_id}] + + topo_diff = Topo_Diff(node_set_add=n_set, + link_set_add=l_set) + + op = dbc.DBO_topo_diff_commit(topo_diff) + self.db_ctl.exec_op(op) + + op = dbc.DBO_rm_node_set([n_0_id, n_1_id]) + self.db_ctl.exec_op(op) + + op = dbc.DBO_rm_node_set([n_2_id, n_3_id], rm_links=True) + self.db_ctl.exec_op(op) + + # assert all deleted + op = dbc.DBO_match_node_id_set(filter_label=n_T) + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_rz_clone(self): + l_n, l_r = gen_rand_data(self.db_ctl, lim_n=8, lim_r=16, prob_link_create=0.7) + op = dbc.DBO_rz_clone(filter_label=l_n, limit=32) + ret = self.db_ctl.exec_op(op) + n_set = ret['node_set'] + l_set = ret['link_set'] + + # TODO improve assertions + self.assertTrue(0 < len(n_set)) + self.assertTrue(0 < len(l_set)) + + def tearDown(self): pass + +if __name__ == "__main__": + unittest.main() diff --git a/src/server-tests/test_rhizi_api.py b/src/server-tests/test_rhizi_api.py new file mode 100644 index 00000000..c404edba --- /dev/null +++ b/src/server-tests/test_rhizi_api.py @@ -0,0 +1,84 @@ +import unittest +import db_controller as dbc +import rhizi_api +import json +import logging + +from rhizi_server import Config +from werkzeug.test import EnvironBuilder +from werkzeug.test import Client + +from db_controller import DB_Driver_Embedded + +class TestRhiziAPI(unittest.TestCase): + + def setUp(self): + self.flush_db() + + @classmethod + def setUpClass(self): + cfg = Config.init_from_file('res/etc/rhizi-server.conf') + self.db_ctl = dbc.DB_Controller(cfg) + rhizi_api.db_ctl = self.db_ctl + + # TODO extract to superclass + log = logging.getLogger('rhizi') + log.setLevel(logging.DEBUG) + log_handler_c = logging.StreamHandler() + log.addHandler(log_handler_c) + + def flush_db(self): + """ + complete DB flush: remove all nodes & links + """ + self.db_ctl.exec_cypher_query('match (n) optional match (n)-[r]-() delete n,r') + + def test_add_node_set(self): + """ + add node set test + """ + node_map = { 'Skill': [{ 'name': 'kung-fu' }, { 'name': 'judo' }] } + with rhizi_api.webapp.test_client() as c: + req = c.post('/add/node-set', + content_type='application/json', + data=json.dumps(dict(node_map=node_map))) + id_set = json.loads(req.data)['data'] + self.assertEqual(2, len(id_set)) + self.assertTrue(isinstance(id_set[0], int)) + + def test_load_node_non_existing(self): + """ + loading a non existing node test + """ + id_set = ['non_existing_id'] + with rhizi_api.webapp.test_client() as c: + req = c.post('/load/node-set-by-id', + content_type='application/json', + data=json.dumps({ 'id_set': id_set})) + req_data = json.loads(req.data) + rz_data = req_data['data'] + rz_err = req_data['error'] + self.assertEqual(None, rz_err) + self.assertEqual(0, len(rz_data)) + + def test_load_node_set_by_id_existing(self): + """ + loading an existing node test + """ + id_set = ['skill_00'] + self.db_ctl.exec_cypher_query('create (s:Skill {id: \'skill_00\'} )') + + with rhizi_api.webapp.test_client() as c: + req = c.post('/load/node-set-by-id', + content_type='application/json', + data=json.dumps({ 'id_set': id_set})) + n_set = json.loads(req.data)['data'] + + self.assertEqual(1, len(n_set)) + self.assertEqual(n_set[0]['id'], id_set[0]) + + def test_load_node_set(self): + pass + +if __name__ == "__main__": + unittest.main() diff --git a/src/server/crypt_util.py b/src/server/crypt_util.py new file mode 100644 index 00000000..464fac94 --- /dev/null +++ b/src/server/crypt_util.py @@ -0,0 +1,46 @@ +import pickle +import hashlib, uuid +import os +import logging + +log = logging.getLogger('rhizi') + +def add_user_login(config, u, p): + htpasswd_path = config.htpasswd_path + + if False == os.path.exists(htpasswd_path): + with open(htpasswd_path, 'wb') as f: + pickle.dump({}, f) + + with open(htpasswd_path, 'rb') as f: + data = f.read() + pw_db = pickle.loads(data) + + with open(htpasswd_path, 'wb') as f: + salt = config.secret_key + pw_db[u] = hash_pw(str(p), salt) + pickle.dump(pw_db, f) + + log.info('htpasswd db: added entry: user: %s, pw: %s...' % (u, pw_db[u][:5])) + +def hash_pw(pw_str, salt_str): + salt = hashlib.sha512(salt_str).hexdigest() + ret = hashlib.sha512(pw_str + salt).hexdigest() + return ret + +def validate_login(config, u, p): + htpasswd_path = config.htpasswd_path + + salt = config.secret_key + + with open(htpasswd_path) as f: + pw_db = pickle.load(f) + + existing_pw_hash = pw_db.get(u) + if None == existing_pw_hash: + raise Exception('Not autorhized') + + if hash_pw(p, salt) != existing_pw_hash: + raise Exception('Not autorhized') + + diff --git a/src/server/db_controller.py b/src/server/db_controller.py new file mode 100644 index 00000000..c17fd0cd --- /dev/null +++ b/src/server/db_controller.py @@ -0,0 +1,495 @@ +#!/usr/bin/python + +import json +import logging +import os +import re +import traceback + +from db_driver import DB_Driver_REST, DB_Driver_Base +from model.graph import Attr_Diff +from model.graph import Topo_Diff +from neo4j_util import DB_result_set +from neo4j_util import cfmt +import neo4j_util as db_util +from model.model import Link + +log = logging.getLogger('rhizi') + +class DB_op(object): + """ + tx wrapped DB operation possibly composing multiple DB queries + """ + def __init__(self): + self.statement_set = [] + self.result_set = [] + self.error_set = None + self.tx_id = None + self.tx_commit_url = None # cached from response to tx begin + + def parse_tx_id(self, tx_commit_url): + m = re.search('/(?P\d+)/commit$', tx_commit_url) + id_str = m.group('id') + self.tx_id = int(id_str) + + def add_statement(self, query, query_params={}): + """ + add a DB query language statement + @return: statement index (zero based) + """ + s = db_util.statement_to_REST_form(query, query_params) + self.statement_set.append(s) + return len(self.statement_set) + + def __iter__(self): + """ + iterate over (statement_index, statement, result, error) + where result & error are mutually exclusive + + note: statement_index is zero based + + TODO: handle partial iteration due to error_set being non-empty + """ + i = 0 + r_set_len = len(self.result_set) + for s in self.statement_set: + r_set = None # row-set + if i < r_set_len: # support partial result recovery + r_set = DB_result_set(self.result_set[i]) + yield (i, s, r_set) + i = i + 1 + + def parse_multi_statement_response_data(self, data): + pass + + @property + def name(self): + return self.__class__.__name__ + + def process_result_set(self): + """ + DB op can issue complex sets of quries all at once - this helper method + assists in parsing response data from a single query. + """ + ret = [] + for _, _, r_set in self: + for row in r_set: + for col in row: + ret.append(col) + return ret + +class DB_composed_op(DB_op): + def __init__(self): + super(DB_composed_op, self).__init__() + self.sub_op_set = [] + + def __assert_false_statement_access(self): + assert False, "composed_op may not contain statements, only sub-ops" + + def add_statement(self, query, query_params={}): + self.__assert_false_statement_access() + + def add_sub_op(self, op): + self.sub_op_set.append(op) + + def __getattribute__(self, attr): + """ + intercept 'statement_set' attr get + """ + if attr == 'statement_set': + self.__assert_false_statement_access() + + return object.__getattribute__(self, attr) + + def __iter__(self): + """ + iterate over sub_op_set + """ + for s_op in self.sub_op_set: + yield s_op + + def process_result_set(self): + ret = [] + for s_op in self: + s_result_set = s_op.process_result_set() + ret.append(s_result_set) + return ret + +class DBO_cypher_query(DB_op): + """ + freeform cypher query + """ + def __init__(self, q, q_params={}): + super(DBO_cypher_query, self).__init__() + self.add_statement(q, q_params) + +class DBO_topo_diff_commit(DB_composed_op): + """ + commit a Topo_Diff + """ + def __init__(self, topo_diff): + super(DBO_topo_diff_commit, self).__init__() + + n_add_map = db_util.meta_attr_list_to_meta_attr_map(topo_diff.node_set_add) + l_add_map = db_util.meta_attr_list_to_meta_attr_map(topo_diff.link_set_add) + l_rm_set = topo_diff.link_set_rm + n_rm_set = topo_diff.node_set_rm + + # + # [!] order critical + # + if len(n_add_map) > 0: + op = DBO_add_node_set(n_add_map) + self.add_sub_op(op) + + if len(l_add_map) > 0: + op = DBO_add_link_set(l_add_map) + self.add_sub_op(op) + + if len(l_rm_set) > 0: + op = DBO_rm_link_set(l_rm_set) + self.add_sub_op(op) + + if len(n_rm_set) > 0: + op = DBO_rm_node_set(n_rm_set) + self.add_sub_op(op) + +class DBO_attr_diff_commit(DB_op): + """ + commit a Attr_Diff + """ + def __init__(self, attr_diff): + super(DBO_attr_diff_commit, self).__init__() + + for id_attr, n_attr_diff in attr_diff.type__node.items(): + # TODO parameterize multiple attr removal + r_attr_set = n_attr_diff['__attr_remove'] + w_attr_set = n_attr_diff['__attr_write'] + + assert len(r_attr_set) > 0 or len(w_attr_set) > 0 + + q_arr = ["match (n {id: {id}}) ", + "return n.id, n"] + q_param_set = {'id': id_attr} + + if len(r_attr_set) > 0: + stmt_attr_rm = "remove " + ', '.join(['n.' + attr for attr in r_attr_set]) + q_arr.insert(1, stmt_attr_rm) + + if len(w_attr_set) > 0: + stmt_attr_set = "set n += {attr_set}" + q_arr.insert(1, stmt_attr_set) + q_param_set['attr_set'] = w_attr_set + + q = " ".join(q_arr) + self.add_statement(q, q_param_set) + + for id_attr, n_attr_diff in attr_diff.type__link.items(): + pass # TODO: handl link attr_diffs + + def process_result_set(self): + ret = {} + for _, _, r_set in self: + for row in r_set: + n_id, n = [v for v in row] # we expect a [n_id, n] array + ret[n_id] = n + return ret + +class DBO_add_node_set(DB_op): + def __init__(self, node_map): + """ + DB op: add node set + + @param node_map: node-type to node-set map + @return: set of new node DB ids + """ + super(DBO_add_node_set, self).__init__() + for q, q_param_set in db_util.gen_query_create_from_node_map(node_map): + self.add_statement(q, q_param_set) + + def process_result_set(self): + id_set = [] + for _, _, row_set in self: + for row in row_set: + for clo in row: + id_set.append(clo) + + return id_set + +class DBO_add_link_set(DB_op): + def __init__(self, link_map): + """ + @param link_map: is a link-type to link-set map - see model.link + @return: set of new node DB ids + """ + super(DBO_add_link_set, self).__init__() + for q, q_params in db_util.gen_query_create_from_link_map(link_map): + self.add_statement(q, q_params) + + def process_result_set(self): + id_set = [] + for _, _, r_set in self: + for row in r_set: + for col_val in row: + id_set.append(col_val) + + return id_set + +class DBO_load_node_set_by_DB_id(DB_op): + def __init__(self, id_set): + """ + load a set of nodes whose DB id is in id_set + + @param id_set: DB node id set + @return: loaded node set or an empty set if no match was found + """ + super(DBO_load_node_set_by_DB_id, self).__init__() + q = "start n=node({id_set}) return n" + self.add_statement(q, { 'id_set': id_set}) + +class DBO_match_node_id_set(DB_op): + + def __init__(self, filter_label=None, filter_attr_map={}): + """ + match a set of nodes by type / attr_map + + @param filter_label: node type filter + @param filter_attr_map: is a filter_key to filter_value_set map of + possible attributes to match against, eg.: + { 'id':[0,1], 'color: ['red','blue'] } + @return: a set of node DB id's + """ + super(DBO_match_node_id_set, self).__init__() + + q = "match (n{filter_label}) {where_clause} return id(n)" + q = cfmt(q, filter_label="" if not filter_label else ":" + filter_label) + q = cfmt(q, where_clause=db_util.gen_clause_where_from_filter_attr_map(filter_attr_map)) + + q_params = filter_attr_map + + self.add_statement(q, q_params) + +class DBO_match_node_set_by_id_attribute(DBO_match_node_id_set): + def __init__(self, id_set): + """ + convenience op: load a set of nodes by their 'id' attribute != DB node id + """ + assert isinstance(id_set, list) + + super(DBO_match_node_set_by_id_attribute, self).__init__(filter_attr_map={'id': id_set}) + + +class DBO_load_link_set(DB_op): + def __init__(self, link_ptr_set): + """ + match a set of sets of links by source/target node id attributes + + This class should be instantiated through a static factory function + + @link_ptr_set link pointer set + @return: a set of loaded links + """ + super(DBO_load_link_set, self).__init__() + + for l_ptr in link_ptr_set: + if not l_ptr.src_id: + q = "match ()-[r]->({id: {dst_id}}) return r" + q_params = {'dst_id': l_ptr.dst_id} + elif not l_ptr.dst_id: + q = "match ({id: {src_id}})-[r]->() return r" + q_params = {'src_id': l_ptr.src_id} + else: + q = "match ({id: {src_id}})-[r]->({id: {dst_id}}) return r" + q_params = {'src_id': l_ptr.src_id, 'dst_id': l_ptr.dst_id} + + self.add_statement(q, q_params) + + @staticmethod + def init_from_link_ptr(l_ptr): + return DBO_load_link_set([l_ptr]) + + @staticmethod + def init_from_link_ptr_set(l_ptr_set): + return DBO_load_link_set(l_ptr_set) + +class DBO_match_link_id_set(DB_op): + def __init__(self, filter_label=None, filter_attr_map={}): + """ + load an id-set of links + + @param filter_label: link type filter + @param filter_attr_map: is a filter_key to filter_value_set map of + attributes to match link properties against + @return: a set of loaded link ids + """ + super(DBO_match_link_id_set, self).__init__() + + q_arr = ['match ()-[r{filter_label} {filter_attr}]->()', + 'return id(r)' + ] + q = ' '.join(q_arr) + q = cfmt(q, filter_label="" if not filter_label else ":" + filter_label) + q = cfmt(q, filter_attr=db_util.gen_clause_attr_filter_from_filter_attr_map(filter_attr_map)) + q_params = {k: v[0] for (k, v) in filter_attr_map.items()} # pass on only first value from each value set + + self.add_statement(q, q_params) + +class DBO_rm_node_set(DB_op): + def __init__(self, id_set, rm_links=False): + """ + remove node set + """ + assert len(id_set) > 0, __name__ + ': empty id set' + + super(DBO_rm_node_set, self).__init__() + + if rm_links: + q_arr = ['match (n)', + 'where n.id in {id_set}', + 'optional match (n)-[r]-()', + 'delete n,r', + 'return {id_set}' + ] + else: + q_arr = ['match (n)', + 'where n.id in {id_set}', + 'delete n', + 'return {id_set}' + ] + + q = ' '.join(q_arr) + q_params = {'id_set': id_set} + self.add_statement(q, q_params) + +class DBO_rm_link_set(DB_op): + def __init__(self, id_set): + """ + remove link set + + [!] when removing as a result of node removal, use DBO_rm_node_set + along with rm_links=True + """ + assert len(id_set) > 0, __name__ + ': empty id set' + + super(DBO_rm_link_set, self).__init__() + + q_arr = ['match ()-[r]->()', + 'where r.id in {id_set}', + 'delete r', + 'return {id_set}' + ] + + q = ' '.join(q_arr) + q_params = {'id_set': id_set} + self.add_statement(q, q_params) + +class DBO_rz_clone(DB_op): + def __init__(self, filter_label=None, limit=128): + """ + clone rhizi + + @return: a dict: {'node_set': n_set, + 'link_set': l_set } + where l_set is a list of (src.id, dst.id, link) tuples + """ + super(DBO_rz_clone, self).__init__() + + self.limit = limit + self.skip = 0 + + q_arr = ['match (n)' if not filter_label else 'match (n:%s)' % (filter_label), + 'optional match (n)-[r]->(m)', + 'with n,r,m', + 'order by n.id', + 'skip %d' % (self.skip), + 'limit %d' % (self.limit), + 'return n,labels(n),collect([m.id, r, type(r)])'] + + q = ' '.join(q_arr) + self.add_statement(q) + + def process_result_set(self): + ret_n_set = [] + ret_l_set = [] + for _, _, row_set in self: + for row in row_set: + n, n_lbl_set, l_set = row.items() # see query return statement + + # reconstruct nodes + assert None != n['id'] + + n['__label_set'] = n_lbl_set + ret_n_set.append(n) + + # reconstruct links from link tuples + for l_tuple in l_set: + assert 3 == len(l_tuple) # see query return statement + + if None == l_tuple[0]: # check if link dst is None + # as link matching is optional, collect may yield empty sets + continue + + l = l_tuple[1] + l['__src_id'] = n['id'] + l['__dst_id'] = l_tuple[0] + l['__label_set'] = [l_tuple[2]] # box single value returned by type() + + ret_l_set.append(l) + + return {'node_set': ret_n_set, + 'link_set': ret_l_set } + +class DB_Controller: + """ + neo4j DB controller + """ + def __init__(self, config, db_driver_class=None): + self.config = config + if not db_driver_class: + self.db_driver = DB_Driver_REST(self.config.db_base_url) + else: + self.db_driver = db_driver_class() + assert isinstance(self.db_driver, DB_Driver_Base) + + def exec_op(self, op): + """ + execute operation within a DB transaction + """ + if isinstance(op, DB_composed_op): + # construct a list comprehension composed of all sup_op statements + for s_op in op.sub_op_set: + self.exec_op(s_op) + return op.process_result_set() + + try: + self.db_driver.begin_tx(op) + self.db_driver.exec_statement_set(op) + self.db_driver.commit_tx(op) + + ret = op.process_result_set() + + log.debug('exec_op:' + op.name + ': return value: ' + str(ret)) + return ret + except Exception as e: + # here we watch for IOExecptions, etc - not db errors + # these are returned in the db response itself + log.error(e.message) + log.error(traceback.print_exc()) + raise e + + def create_db_op(self, f_work, f_cont): + ret = DB_op(f_work, f_cont) + return ret + + def exec_cypher_query(self, q): + """ + @deprecated: use DBO_cypher_query + """ + + # call post and not db_util.post_neo4j to avoid response key errors + try: + db_util.post(self.config.db_base_url + '/db/data/cypher', {"query" : q}) + except Exception as e: + log.error(e.message) + log.error(traceback.print_exc()) + raise e diff --git a/src/server/db_driver.py b/src/server/db_driver.py new file mode 100644 index 00000000..0532e512 --- /dev/null +++ b/src/server/db_driver.py @@ -0,0 +1,91 @@ +import logging + +from neo4j_util import Neo4JException +import neo4j_util as db_util + + +log = logging.getLogger('rhizi') + +class DB_Driver_Base(): + + def log_committed_queries(self, statement_set): + for sp_dict in statement_set['statements']: + if None != sp_dict['parameters']: + msg = '\tq: {0}\n\tp: {1}'.format(sp_dict['statement'], + sp_dict['parameters']) + else: + msg = '\tq: {0}'.format(sp_dict['statement']) + log.debug(msg) + +class DB_Driver_Embedded(DB_Driver_Base): + def __init__(self, db_base_url): + self.tx_base_url = db_base_url + '/db/data/transaction' + + from org.rhizi.db.neo4j.util import EmbeddedNeo4j + self.edb = EmbeddedNeo4j.createDb() + self.edb.createDb() + + def begin_tx(self, op): + pass + + def exec_statement_set(self, op): + s_set = op.statement_set + self.edb.executeCypherQury() + + def commit_tx(self, op): + pass + +class DB_Driver_REST(DB_Driver_Base): + def __init__(self, db_base_url): + self.tx_base_url = db_base_url + '/db/data/transaction' + + def begin_tx(self, op): + tx_open_url = self.tx_base_url + + try: + # + # [!] neo4j seems picky about receiving an additional empty statement list + # + data = data = db_util.statement_set_to_REST_form([]) + ret = db_util.post_neo4j(tx_open_url, data) + tx_commit_url = ret['commit'] + op.parse_tx_id(tx_commit_url) + + log.debug('tx-open: id: {0}, commit-url: {1}'.format(op.tx_id, tx_commit_url)) + except Exception as e: + raise Exception('failed to open transaction:' + e.message) + + def exec_statement_set(self, op): + + tx_url = "{0}/{1}".format(self.tx_base_url, op.tx_id) + statement_set = db_util.statement_set_to_REST_form(op.statement_set) + + try: + post_ret = db_util.post_neo4j(tx_url, statement_set) + op.result_set = post_ret['results'] + op.error_set = post_ret['errors'] + if 0 != len(op.error_set): + raise Neo4JException(op.error_set) + + self.log_committed_queries(statement_set) + except Neo4JException as e: + raise e + except Exception as e: + raise Exception('failed exec op statements: err: {0}, url: {1}'.format(e.message, tx_url)) + + def commit_tx(self, op): + tx_commit_url = "{0}/{1}/commit".format(self.tx_base_url, op.tx_id) + + try: + # + # [!] neo4j seems picky about receiving an additional empty statement list + # + data = db_util.statement_set_to_REST_form([]) + ret = db_util.post(tx_commit_url, data) + + log.debug('tx-commit: id: {0}, commit-url: {1}'.format(op.tx_id, tx_commit_url)) + + return ret + except Exception as e: + raise Exception('failed to commit transaction:' + e.message) + diff --git a/src/server/model/__init__.py b/src/server/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/server/model/graph.py b/src/server/model/graph.py new file mode 100644 index 00000000..f197921d --- /dev/null +++ b/src/server/model/graph.py @@ -0,0 +1,105 @@ +class Attr_Diff(dict): + """ + Represents a change to note attributes, where nodes can represent + either logical nodes or logical links, and attributes can be added, + changed or removed + + Example: + attr_diff = {'__type_node' : {n_id: {'__attr_write': {'attr_0': 0, + 'attr_1': 'a'}, + '__attr_remove': ['attr_2'] }} + '__type_link' : {l_id: ... } + } + """ + def __init__(self): + self['__type_node'] = {} + self['__type_link'] = {} + + def init_node_attr_diff(self, n_id): + ret = {'__attr_write': {}, + '__attr_remove': []} + self['__type_node'][n_id] = ret + return ret + + @staticmethod + def from_json_dict(json_dict): + ret = Attr_Diff() + for obj_type in ret.keys(): + obj_ad_set = json_dict.get(obj_type) + if None != obj_ad_set: + for o_id, ad in obj_ad_set.items(): + if None != ad.get('__attr_write'): + for k, v in ad['__attr_write'].items(): + ret.add_node_attr_write(o_id, k, v) + if None != ad.get('__attr_remove'): + for k in ad['__attr_remove']: + ret.add_node_attr_rm(o_id, k) + return ret + + @property + def type__node(self): + return self['__type_node'] + + @property + def type__link(self): + return self['__type_link'] + + def add_node_attr_write(self, n_id, attr_name, attr_val): + + assert 'id' != attr_name.lower(), 'Attr_Diff: attempt to write to \'id\' attribute' + + n_attr_diff = self['__type_node'].get(n_id) + if None == n_attr_diff: + n_attr_diff = self.init_node_attr_diff(n_id) + n_attr_diff['__attr_write'][attr_name] = attr_val + + def add_node_attr_rm(self, n_id, attr_name): + n_attr_diff = self['__type_node'].get(n_id) + if None == n_attr_diff: + n_attr_diff = self.init_node_attr_diff(n_id) + n_attr_diff['__attr_remove'].append(attr_name) + + def add_link_attr_write(self, l_id, attr_name, attr_val): + assert False, 'unimplemented' + + def add_link_attr_rm(self, l_id, attr_name): + assert False, 'unimplemented' + +class Topo_Diff(object): + """ + Represents a change to the graph topology + """ + def __init__(self, link_set_rm=[], + node_set_rm=[], + node_set_add=[], + link_set_add=[]): + + self.link_set_rm = link_set_rm + self.node_set_rm = node_set_rm + self.node_set_add = node_set_add + self.link_set_add = link_set_add + + def __str__(self): + return __name__ + ': ' + ', '.join('%s: %s' % (k, v) for k, v in self.__dict__.items()) + + def check_validity(self, topo_diff_dict): + """ + Topo_Diff may represent invalid operations, eg. adding a link while + removing it's end-point - this stub should check for that + """ + pass + + @staticmethod + def from_json_dict(json_dict): + """ + construct from dict - no node/link constructor set must be provided + """ + ret = Topo_Diff() + + # merge keys - this allows constructor argument omission (link_set_rm, + # node_set_rm, etc.) such as when constructing from POST JSON data + for k, _ in ret.__dict__.items(): + v = json_dict.get(k) + if None != v: + ret.__dict__[k] = v + return ret diff --git a/src/server/model/model.py b/src/server/model/model.py new file mode 100644 index 00000000..33cf9e6a --- /dev/null +++ b/src/server/model/model.py @@ -0,0 +1,37 @@ +class Link(): + """ + documentation anchor - this class currently carries no implementation + and only acts as a documentation anchor + + link['__src'] - meta attribute for link source object + link['__dst'] - meta attribute for link destination object + """ + + def __init__(self, src=None, dst=None): + assert False, 'currently unused' + + class Link_Ptr(dict): + """ + link['__src_id'] - meta attribute for link source id + link['__dst_id'] - meta attribute for link destination id + """ + def __init__(self, src_id=None, dst_id=None): + assert None != src_id or None != dst_id + + self['__src_id'] = src_id + self['__dst_id'] = dst_id + + @property + def src_id(self): + return self['__src_id'] + + @property + def dst_id(self): + return self['__dst_id'] + + @staticmethod + def link_ptr(src_id=None, dst_id=None): + """ + init from src_id or dst_id attributes - at least one must be provided + """ + return Link.Link_Ptr(src_id, dst_id) diff --git a/src/server/neo4j_util.py b/src/server/neo4j_util.py new file mode 100644 index 00000000..cf2f962e --- /dev/null +++ b/src/server/neo4j_util.py @@ -0,0 +1,260 @@ +""" + Utility code in speaking the neo4j REST api +""" + +import json +import six +from six.moves.urllib import request +import six.moves.urllib_error as urllib_error +import model +import string +import time + +from util import debug_log_duration + +class Neo4JException(Exception): + def __init__(self, error_set): + self.error_set = error_set + + def __str__(self): + return 'neo4j error set: ' + str(self.error_set) + +class DB_row(object): + def __init__(self, data): + self.data = data + + def __iter__(self): + for column_val in self.data: + yield column_val + + def items(self): + return [x for x in self] + +class DB_result_set(object): + def __init__(self, data): + self.data = data + + def __iter__(self): + for db_row_dict in self.data['data']: + # example: dict: {u'row': [{u'title': u'foo'}]} + assert None != db_row_dict['row'] + + yield DB_row(db_row_dict['row']) + + def items(self): + return [x for x in self] + +class Cypher_String_Formatter(string.Formatter): + """ + Despite parameter support in Cypher, we sometimes do engage in query string building + - as both Cypher & Python use brackets to wrap parameters, escaping them in Python makes + queries less readable. This customized formatter will simply ignore unavailable keyworded + formatting arguments, allowing the use of non-escaped parameter designation, eg: + q = cfmt("match (a:{type} {cypher_param})", type='Book') + """ + + def get_field(self, field_name, args, kwargs): + # ignore key not found, return bracket wrapped key + try: + val = super(Cypher_String_Formatter, self).get_field(field_name, args, kwargs) + except (KeyError, AttributeError): + val = "{" + field_name + "}", field_name + return val + +def cfmt(fmt_str, *args, **kwargs): + return Cypher_String_Formatter().format(fmt_str, *args, **kwargs) + +def post_neo4j(url, data): + """ + @return dict object from the neo4j json POST response + """ + ret = post(url, data) + ret_data = json.load(ret) + + # [!] do not raise exception if ret_data['errors'] is not empty - + # this allows query-sets to partially succeed + + return ret_data + +def post(url, data): + assert(isinstance(data, dict)) # make sure we're not handed json strings + + post_data_json = json.dumps(data) + + req = request.Request(url) + req.add_header('User-Agent', 'rhizi-server/0.1') + req.add_header('Accept', 'application/json; charset=UTF-8') + req.add_header('Content-Type', 'application/json') + + req.add_header('X-Stream', 'true') # enable neo4j JSON streaming + + try: + ret = request.urlopen(req, post_data_json) + except urllib_error.HTTPError as e: + raise Exception('post request failed: code: {0}, reason: {1}'.format(e.code, e.reason)) + + return ret + +def statement_to_REST_form(query, parameters={}): + """ + turn cypher query to neo4j json API format + """ + assert isinstance(query, six.string_types) + if isinstance(parameters, list): + for v in parameters: + assert isinstance(v, dict) + else: + assert isinstance(parameters, dict) + + return {'statement' : query, 'parameters': parameters} + +def statement_set_to_REST_form(statement_set): + assert isinstance(statement_set, list) + + return {'statements': statement_set} + +def gen_clause_attr_filter_from_filter_attr_map(filter_attr_map, node_label="n"): + if not filter_attr_map: + return "{}" + + __type_check_filter_attr_map(filter_attr_map) + + filter_arr = [] + for attr_name in filter_attr_map.keys(): + # create a cypher query parameter place holder for each attr set + # eg. n.foo in {foo}, where foo is passed as a query parameter + f_attr = cfmt("{attr_name}: {{{attr}}}", attr_name=attr_name) + filter_arr.append(f_attr) + + filter_str = "{{{0}}}".format(', '.join(filter_arr)) + return filter_str + +def gen_clause_where_from_filter_attr_map(filter_attr_map, node_label="n"): + """ + convert a filter attribute map to a parameterized Cypher where clause, eg. + in: { 'att_foo': [ 'a', 'b' ], 'att_goo': [1,2] } + out: {att_foo: {att_foo}, att_goo: {att_goo}, ...} + + this function will essentially ignore all but the first value in the value list + + @param filter_attr_map: may be None or empty + """ + if not filter_attr_map: + return "" + + __type_check_filter_attr_map(filter_attr_map) + + filter_arr = [] + for attr in filter_attr_map.keys(): + # create a cypher query parameter place holder for each attr set + # eg. n.foo in {foo}, where foo is passed as a query parameter + f_attr = cfmt("{node_label}.{attr} in {{{attr}}}", node_label=node_label, attr=attr) + filter_arr.append(f_attr) + filter_str = "where {0}".format(' and '.join(filter_arr)) + return filter_str + +def gen_query_create_from_node_map(node_map, input_to_DB_property_map=lambda _: _): + """ + generate a set of node create queries + + @param node_map: is a node-type to node map + @input_to_DB_property_map: optional function which takes a map of input properties and returns a map of DB properties - use to map input schemas to DB schemas + + @return: a (query, query_parameteres) set of create queries + """ + __type_check_link_or_node_map(node_map) + + ret = [] + for label, n_set in node_map.items(): + + assert len(label) > 2 and label[0].isupper() and label[1:].islower(), 'malformed label: ' + label + + q_arr = ['create (n:%s {node_attr})' % (label), + 'return n.id' + ] + + q = ' '.join(q_arr) + q_params_set = [] + for n_prop_set in n_set: + + assert None != n_prop_set['id'], 'node create query: node id attribute not set' + + q_params = input_to_DB_property_map(n_prop_set) + q_params_set.append(q_params) + ret.append((q, {'node_attr': q_params_set})) + return ret + +def gen_query_create_from_link_map(link_map, input_to_DB_property_map=lambda _: _): + """ + generate a set of link create queries + + @param link_map: is a link-type to link map - see model.link + """ + __type_check_link_or_node_map(link_map) + + ret = [] + for l_type, l_set in link_map.items(): + q = "match (src {id: {src}.id}),(dst {id: {dst}.id}) " + \ + "create (src)-[r:%(__type)s {link_attr}]->(dst) " + \ + "return id(r)" + q = q % {'__type':l_type} + + for link in l_set: + __type_check_link(link) + + src_id = link['__src_id'] + dst_id = link['__dst_id'] + + # TODO: use object based link representation + prop_dict = link.copy() + del prop_dict['__dst_id'] + del prop_dict['__src_id'] + + q_params = {'src': { 'id': src_id} , + 'dst': { 'id': dst_id} , + 'link_attr' : input_to_DB_property_map(prop_dict)} + ret.append((q, q_params)) + + return ret + +def meta_attr_list_to_meta_attr_map(e_set, meta_attr='__label_set'): + """ + convert a list of maps each containing a meta_attr key into a + meta_attr-mapped collection of lists with the meta_attr removed - eg: + + in: [{'id':0, '__type': 'T'}, {'id':1, '__type': 'T'}] + out: { 'T', [{'id':0}, {'id':1}] } + """ + ret = {} + for v in e_set: + assert None != v[meta_attr], 'missing type meta-attribute' + assert 1 == len(v[meta_attr]), 'only single-type mapping currently suppoerted' + + v_type = v[meta_attr][0] + if None == ret.get(v_type): # init type list if necessary + ret[v_type] = [] + + v_no_meta = v.copy() + del v_no_meta[meta_attr] + + ret[v_type].append(v_no_meta) + + return ret + +def __type_check_link(link): + assert link.has_key('__src_id') + assert link.has_key('__dst_id') + +def __type_check_link_or_node_map(x_map): + for k, v in x_map.iteritems(): # do some type sanity checking + assert isinstance(k, six.string_types) + assert isinstance(v, list) + +def __type_check_filter_attr_map(filter_attr_map): + """ + # type sanity check an attribute filter map + """ + assert isinstance(filter_attr_map, dict) + for k, v in filter_attr_map.items(): + assert isinstance(k, six.string_types) + assert isinstance(v, list) diff --git a/src/server/rhizi_api.py b/src/server/rhizi_api.py new file mode 100644 index 00000000..579d41e1 --- /dev/null +++ b/src/server/rhizi_api.py @@ -0,0 +1,256 @@ +""" +Rhizi web API +""" +import os +import db_controller as dbc +import json +import logging +import traceback +import crypt_util + +import flask +from flask import jsonify +from flask import Flask +from flask import request +from flask import make_response +from flask import session +from flask import redirect +from flask import escape +from flask import url_for +from flask import render_template +from flask import send_from_directory + +from model.graph import Topo_Diff +from model.graph import Attr_Diff +from model.model import Link +from datetime import datetime + +log = logging.getLogger('rhizi') + +# injected: DB controller +db_ctl = None + +def __sanitize_input(*args, **kw_args): + pass + +def sanitize_input__node(n): + """ + provide a control point as to which node fields are persisted + """ + assert None != n['id'], 'invalid input: node: missing id' + +def sanitize_input__link(l): + """ + provide a control point as to which node fields are persisted + """ + assert None != l['id'], 'invalid input: link: missing id' + assert None != l['__src_id'], 'invalid input: link: missing src id' + assert None != l['__dst_id'], 'invalid input: link: missing dst id' + +def sanitize_input__topo_diff(topo_diff): + for n in topo_diff.node_set_add: + sanitize_input__node(n) + for l in topo_diff.link_set_add: + sanitize_input__link(l) + +def sanitize_input__attr_diff(attr_diff): + pass # TODO: impl + +def __response_wrap(data=None, error=None): + """ + wrap response data/errors as dict - this should always be used when returning + data to allow easy return of list objects, assist in error case distinction, etc. + """ + return dict(data=data, error=error) + +def __common_resp_handle(data=None, error=None): + """ + provide common response handling + """ + ret_data = __response_wrap(data, error) + resp = jsonify(ret_data) + + resp.headers['Access-Control-Allow-Origin'] = '*' + + # more response processing + + return resp + +def __common_exec(op, on_success=__common_resp_handle): + try: + op_ret = db_ctl.exec_op(op) + return on_success(op_ret) + except Exception as e: + log.error(e.message) + log.error(traceback.print_exc()) + return __common_resp_handle('error occurred') + +def load_node_set_by_id_attr(): + """ + load node-set by ID attribute + + @param id_set: list of node ids to match id attribute against + @return: a list of nodes whose id attribute matches 'id' or + an empty list if the requested node is not found + @raise exception: on error + """ + req_json = request.get_json() + id_set = req_json['id_set'] + + __sanitize_input(id_set) + + return __load_node_set_by_id_attr_common(id_set) + +def __load_node_set_by_id_attr_common(id_set): + """ + @param f_k: optional attribute filter key + @param f_vset: possible key values to match against + """ + op = dbc.DBO_match_node_set_by_id_attribute(id_set=id_set) + try: + n_set = db_ctl.exec_op(op) + return __common_resp_handle(data=n_set) + except Exception as e: + log.exception(e) + return __common_resp_handle(error='unable to load node with ids: {0}'.format(id_set)) + +def match_node_set_by_attr_filter_map(attr_filter_map): + """ + @param attr_filter_map + + @return: a set of node DB id's + """ + op = dbc.DBO_match_node_id_set(attr_filter_map) + return __common_exec(op) + +def load_link_set_by_link_ptr_set(): + + def deserialize_param_set(param_json): + l_ptr_set_raw = param_json['link_ptr_set'] + + __sanitize_input(l_ptr_set_raw) + + l_ptr_set = [] + for lptr_dict in l_ptr_set_raw: + src_id = lptr_dict.get('__src_id') + dst_id = lptr_dict.get('__dst_id') + l_ptr_set += [Link.Link_Ptr(src_id=src_id, dst_id=dst_id) ] + + return l_ptr_set + + l_ptr_set = deserialize_param_set(request.get_json()) + + op = dbc.DBO_load_link_set.init_from_link_ptr_set(l_ptr_set) + return __common_exec(op) + +def rz_clone(): + op = dbc.DBO_rz_clone() + return __common_exec(op) + +def diff_commit__set(): + """ + commit a diff set + """ + def sanitize_input(req): + diff_set_dict = request.get_json()['diff_set'] + topo_diff_dict = diff_set_dict['__diff_set_topo'][0] + topo_diff = Topo_Diff.from_json_dict(topo_diff_dict) + + sanitize_input__topo_diff(topo_diff) + return topo_diff; + + topo_diff = sanitize_input(request) + op = dbc.DBO_topo_diff_commit(topo_diff) + return __common_exec(op) + +def diff_commit__topo(): + """ + commit a graph topology diff + """ + def sanitize_input(req): + topo_diff_dict = request.get_json()['topo_diff'] + topo_diff = Topo_Diff.from_json_dict(topo_diff_dict) + + sanitize_input__topo_diff(topo_diff) + return topo_diff; + + topo_diff = sanitize_input(request) + op = dbc.DBO_topo_diff_commit(topo_diff) + return __common_exec(op) + +def diff_commit__attr(): + """ + commit a graph attribute diff + """ + def sanitize_input(req): + attr_diff_dict = request.get_json()['attr_diff'] + attr_diff = Attr_Diff.from_json_dict(attr_diff_dict) + + sanitize_input__attr_diff(attr_diff) + return attr_diff; + + attr_diff = sanitize_input(request) + op = dbc.DBO_attr_diff_commit(attr_diff) + return __common_exec(op) + +def diff_commit__vis(): + pass + +def add_node_set(): + """ + @deprecated: use topo_attr_commit + + @param node_map: node type to node map, eg. { 'Skill': { 'name': 'kung-fu' } } + """ + node_map = request.get_json()['node_map'] + __sanitize_input(node_map) + + op = dbc.DBO_add_node_set(node_map) + return __common_exec(op) + +def monitor__server_info(): + """ + server monitor stub + """ + dt = datetime.now() + return "" + \ + "

Rhizi Server v0.1

" + \ + "date: " + dt.strftime("%Y-%m-%d") + "
" + \ + "time: " + dt.strftime("%H:%M:%S") + "
" + \ + "

" + +def index(): + username = escape(session.get('username')) + return render_template('index.html', username=username) + +def login(): + + def sanitize_input(req): + req_json = request.get_json() + u = req_json['username'] + p = req_json['password'] + return u, p + + if request.method == 'POST': + try: + u, p = sanitize_input(request) + crypt_util.validate_login(flask.current_app.rz_config, u, p) + except Exception as e: + # login failed + log.warn('login: unauthorized: user: %s' % (u)) + return render_template('login.html', login_failed=True) + + # login successful + session['username'] = u + log.debug('login: success: user: %s' % (u)) + return redirect(url_for('index')) + + if request.method == 'GET': + return render_template('login.html') + +def logout(): + # remove the username from the session if it's there + u = session.pop('username', None) + log.debug('logout: success: user: %s' % (u)) + return redirect(url_for('login')) + diff --git a/src/server/rhizi_server.py b/src/server/rhizi_server.py new file mode 100644 index 00000000..168e2f53 --- /dev/null +++ b/src/server/rhizi_server.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +import logging +import json +import util +import os +import neo4j_util +import argparse +import db_controller as dbc +import rhizi_api +import flask +import crypt_util +import re + +from flask import Flask +from flask import session +from flask import redirect +from flask import request +from flask import send_from_directory + +from functools import wraps + +class Config(object): + """ + rhizi-server configuration + + TODO: config option documentation + + htpasswd_path + listen_address + listen_port + neo4j_url + root_path + """ + + @staticmethod + def init_from_file(file_path): + + if False == os.path.exists(file_path): + raise Exception('config file not found: ' + file_path) + + # apply defaults + cfg = {} + cfg['access_control'] = True + cfg['config_dir'] = os.path.abspath(os.path.dirname(file_path)) # bypass prop restriction + cfg['development_mode'] = False + cfg['listen_address'] = '127.0.0.1' + cfg['listen_port'] = 8080 + cfg['root_path'] = os.getcwd() + cfg['static_url_path'] = '/static' + + # Flask keys + cfg['SECRET_KEY'] = '' + + with open(file_path, 'r') as f: + for line in f: + if re.match('(^#)|(\s+$)', line): + continue + + kv_arr = line.split('=') + if 2 != len(kv_arr): + raise Exception('failed to parse config line: ' + line) + + k, v = map(str.strip, kv_arr) + + if None != cfg.get(k): + # apply type conversion based on default value type + type_f = type(cfg[k]) + if bool == type_f: + v = v in ("True", "true") # workaround bool('false') = True + else: + v = type_f(v) + + # [!] we can't use k.lower() as we are loading Flask configuration + # keys which are expected to be capitalized + cfg[k] = v + + ret = Config() + ret.__dict__ = cfg # allows setting of @property attributes + + # validate config + if False == os.path.isabs(ret.root_path): + ret.root_path = os.path.abspath(ret.root_path) + + return ret + + def __str__(self): + return '\n'.join('%s: %s' % (k, v) for k, v in self.__dict__.items()) + + @property + def db_base_url(self): + return self.neo4j_url + + @property + def tx_api_path(self): + return '/db/data/transaction' + + @property + def config_dir_path(self): + return self.config_dir + + @property + def secret_key(self): + return self.SECRET_KEY + +class FlaskExt(Flask): + """ + Flask server customization + """ + + def __init__(self, import_name, *args, **kwargs): + """ + reserved for future use + """ + super(FlaskExt, self).__init__(import_name, *args, **kwargs) + + def before_request(self, *args, **kwargs): + # TODO impl + pass + + def make_default_options_response(self): + ret = Flask.make_default_options_response(self) + + ret.headers['Access-Control-Allow-Origin'] = 'http://rhizi.net' + ret.headers['Access-Control-Allow-Headers'] = "Accept, Authorization, Content-Type, Origin" + ret.headers['Access-Control-Allow-Credentials'] = 'true' + + # ret.headers['Access-Control-Allow-Methods'] = ', '.join(m_list) + return ret + +def init_log(cfg): + """ + init log file, location derived from configuration + """ + log = logging.getLogger('rhizi') + log.setLevel(logging.DEBUG) + log_handler_c = logging.StreamHandler() + log_handler_f = logging.FileHandler(cfg.log_path) + + log.addHandler(log_handler_c) + log.addHandler(log_handler_f) + return log + +def init_rest_api(cfg, flask_webapp): + """ + map REST API calls + """ + + def rest_entry(path, f, flask_args={'methods': ['POST']}): + return (path, f, flask_args) + + def dev_mode__resend_from_static(static_url): + """ + redirect broken-on-local-deploy links: + - /src -> '': handle root based files, eg. app.js + - /res, /lib -> res, lib + """ + static_folder = flask.current_app.static_folder + + static_path = request.path + if static_path.startswith('/src'): + # TODO: clean - /src/... links should not exist + static_path = static_path.replace('/src', '') + if static_path.startswith('/'): # convert to relative path + static_path = static_path[1:] + return send_from_directory(static_folder, static_path) + + def login_decorator(f): + """ + [!] security boundary: asserd logged-in user before executing REST api call + """ + @wraps(f) + def wrapped_function(*args, **kw): + if not 'username' in session: + return redirect('/login') + return f(*args, **kw) + + return wrapped_function + + rest_entry_set = [ + rest_entry('/add/node-set' , rhizi_api.add_node_set), + rest_entry('/graph/clone', rhizi_api.rz_clone), + rest_entry('/graph/diff-commit-set', rhizi_api.diff_commit__set), + rest_entry('/graph/diff-commit-topo', rhizi_api.diff_commit__topo), + rest_entry('/graph/diff-commit-attr', rhizi_api.diff_commit__attr), + rest_entry('/graph/diff-commit-vis', rhizi_api.diff_commit__vis), + rest_entry('/index', rhizi_api.index, {'methods': ['GET']}), + rest_entry('/load/node-set-by-id', rhizi_api.load_node_set_by_id_attr), + rest_entry('/load/link-set/by_link_ptr_set', rhizi_api.load_link_set_by_link_ptr_set), + rest_entry('/login', rhizi_api.login, {'methods': ['GET', 'POST']}), + rest_entry('/logout', rhizi_api.logout, {'methods': ['GET', 'POST']}), + rest_entry('/match/node-set', rhizi_api.match_node_set_by_attr_filter_map), + rest_entry('/monitor/server-info', rhizi_api.monitor__server_info), + ] + + if cfg.development_mode: + dev_path_set = ['/src', '/res', '/lib'] + rest_dev_entry_set = [] + for dev_path in dev_path_set: + rest_dev_entry_set.append(rest_entry(dev_path + '/', + dev_mode__resend_from_static, + {'methods': ['GET']})) + rest_entry_set += rest_dev_entry_set + + if False == cfg.access_control: + log.warn('access control disabled, public access set on all URLs') + + for re_entry in rest_entry_set: + rest_path, f, flask_args = re_entry + + if cfg.access_control and '/login' != rest_path: + # currently require login on all but /login paths + f = login_decorator(f) + + # [!] order seems important - apply route decorator last + route_dec = flask_webapp.route(rest_path, **flask_args) + f = route_dec(f) + + flask_webapp.f = f # assign decorated function + +def init_webapp(cfg): + root_path = cfg.root_path + webapp = FlaskExt(__name__, + static_folder='static', + template_folder=os.path.join(root_path, 'templates'), + static_url_path=cfg.static_url_path) + webapp.config.from_object(cfg) + webapp.root_path = root_path # for some reason calling config.from_xxx() does not have effect + + db_ctl = dbc.DB_Controller(cfg) + rhizi_api.db_ctl = db_ctl + + webapp.rz_config = cfg + return webapp + +def init_config(cfg_dir): + cfg_path = os.path.join(cfg_dir, 'rhizi-server.conf') + cfg = Config.init_from_file(cfg_path) + return cfg + + +if __name__ == "__main__": + + p = argparse.ArgumentParser(description='rhizi-server') + p.add_argument('--config-dir', help='path to Rhizi config dir', default='res/etc') + p.add_argument('--init-htpasswd-db', help='init login htpasswd db', action='store_const', const=True) + args = p.parse_args() + + cfg = init_config(args.config_dir) + log = init_log(cfg) + log.debug('loaded configuration:\n%s' % cfg) + + if args.init_htpasswd_db: + init_pw_db(cfg) + exit(0) + + webapp = init_webapp(cfg) + init_rest_api(cfg, webapp) + + log.info('launching webapp via Flask development server') + webapp.run(host=cfg.listen_address, + port=cfg.listen_port) + diff --git a/src/server/rhizi_server_fcgi.py b/src/server/rhizi_server_fcgi.py new file mode 100755 index 00000000..f0b9a04b --- /dev/null +++ b/src/server/rhizi_server_fcgi.py @@ -0,0 +1,25 @@ +#!/usr/bin/python + +from flup.server.fcgi import WSGIServer +import os +import sys +import cgitb +import rhizi_server + +# sys.path.insert(0, '/srv/www/rhizi/rhizi.net/src-py') + +# enable debugging +cgitb.enable() + +if __name__ == '__main__': + cfg_dir = '/etc/rhizi' + + cfg = rhizi_server.init_config(os.path.join(cfg_dir, 'rhizi-server.conf')) + log = rhizi_server.init_log() + + webapp = rhizi_server.init_webapp(cfg) + rhizi_server.init_rest_api(cfg, webapp) + + log.info('launching webapp via flup.server.fcgi.WSGIServer') + + WSGIServer(webapp).run() diff --git a/src/server/util.py b/src/server/util.py new file mode 100644 index 00000000..e8840114 --- /dev/null +++ b/src/server/util.py @@ -0,0 +1,22 @@ +""" +code with no better place to go +""" +import time + +def debug_log_duration(method): + """ + dubug call durations - use example: + + neo4j_util.post = util.debug_log_duration(neo4j_util.post) + """ + + def timed(*args, **kw): + t_0 = time.time() + result = method(*args, **kw) + t_1 = time.time() + dt = t_1 - t_0 + + print ('%2.2f sec, function: %r' % (dt, method.__name__)) + return result + + return timed diff --git a/src/textanalysis.js b/src/textanalysis.js deleted file mode 100644 index 830c16c1..00000000 --- a/src/textanalysis.js +++ /dev/null @@ -1,501 +0,0 @@ -"use strict"; - -define(['rz_core', 'model/core', 'model/util', 'model/diff', 'rz_bus', 'consts'], -function(rz_core, model_core, model_util, model_diff, rz_bus, consts) { - -var typeindex = 0; -var nodetypes = consts.nodetypes; -var typeStack = []; - -var lastnode; - -var sugg = {}, // suggestions for autocompletion of node names - suggestions_options = new Bacon.Bus(); // TODO: Property: same as bus, but with initial value - -var ANALYSIS_NODE_START = 'ANALYSIS_NODE_START'; -var ANALYSIS_NODE = 'ANALYSIS_NODE' -var ANALYSIS_LINK = 'ANALYSIS_LINK'; - -function selectedType() -{ - return nodetypes[typeindex]; -} - -function autoSuggestAddName(name) -{ - /* note that name can contain spaces - this is ok. We might want to limit this though? */ - sugg[name] = 1; - suggestions_options.push(sugg); -} - -/* up_to_two_renames: - * - * allow one letter or 'new node' to anything changes */ -function up_to_two_renames(graph, old_name, new_name) -{ - var not_one_letter = false; - var k; - /* Allowed renames: - * no change - * s1 is substring of s2 - * older (s1) node being 'new node' - */ - function allowed_rename(s1, s2) - { - return (s1 == s2 || - s1 == 'new node' || - s1.substr(0, s2.length) == s2 || - s2.substr(0, s1.length) == s1); - } - - if (old_name.length != new_name.length) { - console.log('bug: up_to_two_renames: not equal inputs'); - return; - } - if (old_name.length > 2) { - console.log('bug: up_to_two_renames: input length 2 < ' + old_name.length); - return; - } - if (old_name.length == 2) { - if (allowed_rename(old_name[0], new_name[1]) && - allowed_rename(old_name[1], new_name[0])) { - old_name = [old_name[1], old_name[0]]; - } else { - if (!allowed_rename(old_name[0], new_name[0]) || - !allowed_rename(old_name[1], new_name[1])) { - not_one_letter = true; - } - } - } - if (not_one_letter) { - console.log('bug: up_to_two_renames: not one letter changes'); - console.log(old_name); - console.log(new_name); - return; - } - for (k = 0 ; k < old_name.length ; ++k) { - graph.editNameByName(old_name[k], new_name[k]); - } -} - -// TODO: add escape char, i.e. r"bla\"bla" -> ['bla"bla'] -function tokenize(text, node_token, quote) -{ - var segment = [], - subsegment = [], - sentence = [], - quoteword; - var j; - - segment = text.split(node_token); - for (j = 0; j < segment.length; j++) { - if (j !== 0) sentence.push(node_token); - subsegment = segment[j].split(" "); - if (subsegment.length === 0) { - sentence.push(" "); - } - for (var k = 0; k < subsegment.length; k++) { - if (subsegment[k] !== " " && subsegment[k] !== "") { - if (subsegment[k].charAt(0) === quote) { - quoteword = ""; - do { - quoteword += subsegment[k] + ' '; - if(subsegment[k].charAt(subsegment[k].length-1) !== quote)k++; - } while (k < subsegment.length && subsegment[k].charAt(subsegment[k].length - 1) !== quote); - if (subsegment[k] && subsegment[k]!==quoteword.replace(/ /g, "")) { - quoteword += subsegment[k]; - } - sentence.push(quoteword.replace(new RegExp(quote, 'g'), "")); - } else { - sentence.push(subsegment[k]); - } - } - } - } - return sentence; -} - -/* - * textAnalyser - * - * Input: - * @newtext - new sentence - * @finalize - is this an intermediate editing state or are we editing the graph - * - * Output: - * none - * - * Side effect: - * updating graph (global) - * - * Implementation notes: - * There is no well defined grammer. The translation goes from obvious to not - * so much for more complex sentences involving more than two nodes (two '#' - * marks). - * - */ -var textAnalyser = function (newtext, finalize) { - var sentence, - token_set_new_node_names = [], // token set representing new node names - token_set_new_link_names = [], // token set representing new link names - linkindex = 0, - nodeindex = 0, - orderStack = [], - and_count = 0, - prefix = "", - m, - word, - completeSentence, - typesetter, starGraph, - n, - link_hash = {}, - yell_bug = false, // TODO: fix both issues - NODE = "NODE", - LINK = "LINK", - START = "START", - ret = model_diff.new_topo_diff(); - - function addNode(name, type, state) { - if (type === undefined) { - console.log('bug: textanalyser.addNode of type undefined'); - } - var node = model_core.create_node_from_spec( - {'name':name, - 'type':type, - 'state':state}); - - ret.node_set_add.push(node); - } - - function __addLink(src, dst, name, state) { - if (!src || !dst) { - if (yell_bug) { - console.log('bug - adding link (' + src + ', ' + dst + ')'); - } - return; - } - if (link_hash[src] && link_hash[src][dst]) { - if (yell_bug) { - console.log('bug - adding link twice (' + src + ', ' + dst + ')'); - } - return; - } - if (!link_hash[src]) { - link_hash[src] = {}; - } - link_hash[src][dst] = 1; - - var link = {'__src': src, '__dst':dst, 'name':name, 'state':state}; // can't use model_core.create_link_from_spec as src,dst are only names - ret.link_set_add.push(link); - } - - function apply_conjugator_link_logic(link, drop_conjugator_links){ - if (drop_conjugator_links && link.name && (link.name.replace(/ /g,"") === "and")) { - link.state = "temp"; - } - } - - if (newtext.indexOf('#') == -1 || finalize) { - lastnode = null; - } - - //Sentence Sequencing - //Build the words and cuts the main elements - sentence = tokenize(newtext, '#', '"'); - - // build new node,link arrays in order of appearance - for (m = 0; m < sentence.length; m++) { - switch (sentence[m]) { - case "#": - orderStack.push(START); - break; - case "and": - case "+": - case ",": - case "&": - sentence[m] = "and"; - and_count++; - //orderStack.push("AND"); - default: - if (orderStack[orderStack.length - 1] === START) { - orderStack.push(NODE); - token_set_new_node_names.push(sentence[m]); - linkindex++; - } else if (orderStack[orderStack.length - 1] === NODE) { - orderStack.push(LINK); - if (!token_set_new_link_names[linkindex]) { - token_set_new_link_names[linkindex] = sentence[m] + " "; - } else { - token_set_new_link_names[linkindex] += sentence[m] + " "; - } - } else { - if (!token_set_new_link_names[linkindex]) { - token_set_new_link_names[linkindex] = sentence[m] + " "; - } else { - token_set_new_link_names[linkindex] += sentence[m] + " "; - } - } - if (token_set_new_node_names.length === 0) { - prefix += (prefix.length > 0 ? ' ' : '') + sentence[m]; - } - break; - } - } - - starGraph = (token_set_new_link_names.length - and_count) >= 3 || - ((token_set_new_link_names.length - and_count >= 1) && - token_set_new_link_names.length > 2 && - orderStack.length > 1 && - orderStack[orderStack.length - 1] != NODE); - - //PREFIX not null case - put complete sentence in first link. - if (prefix && !starGraph) { - token_set_new_link_names[1] = prefix + " " + token_set_new_node_names[0] + - (token_set_new_link_names[1] !== undefined || token_set_new_node_names[1] !== undefined ? - " " : "") - + (token_set_new_link_names[1] !== undefined ? token_set_new_link_names[1] : "") - + (token_set_new_node_names[1] !== undefined ? token_set_new_node_names[1] : ""); - } - - //WRITE COMPLETE SENTENCE - linkindex = 0; - nodeindex = 0; - word = ""; - completeSentence = prefix.length > 0 ? String(prefix) + " " : ""; - for (m = 0; m < orderStack.length; m++) { - if (orderStack[m] === NODE) { - word += " (" + token_set_new_node_names[nodeindex] + ") "; - completeSentence += token_set_new_node_names[nodeindex] + " "; - nodeindex++; - } else if (orderStack[m] === LINK) { - word += " -->" + token_set_new_link_names[nodeindex] + " --> "; - completeSentence += token_set_new_link_names[nodeindex]; - } - } - completeSentence = completeSentence.trim(); - - //REBUILD GRAPH - linkindex = 0; - nodeindex = 0; - - //CHANGE TO PERMANENT STATE AND UPDATE SUGGESTIONLIST - typesetter = ""; - if (finalize === true) { - typesetter = "perm"; - for (n = 0; n < token_set_new_node_names.length; n++) { - autoSuggestAddName(token_set_new_node_names[n]); - } - } else { - typesetter = "temp"; - } - - //ADD SURROUNDING BUBBLE - if (orderStack.length > 0) { - addNode("", "bubble","temp"); - } - - //0-N ORDER STACK - for (m = 0; m < orderStack.length - 1; m++) { - switch (orderStack[m]) { - case START: - if (!typeStack[nodeindex]) { - typeStack[nodeindex] = selectedType(); - } - break; - case NODE: - addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter); - if (!starGraph && nodeindex > 0) { - __addLink(token_set_new_node_names[nodeindex - 1], - token_set_new_node_names[nodeindex], - token_set_new_link_names[linkindex], typesetter); - } - nodeindex++; - break; - case LINK: - linkindex++; - break; - } - } - - //FINAL N ORDER - switch (orderStack[orderStack.length - 1]) { - case START: - typeStack[nodeindex] = selectedType(); - addNode("new node", typeStack[nodeindex], "temp"); - if (!starGraph && nodeindex > 0) { - __addLink(token_set_new_node_names[nodeindex - 1], "new node", - token_set_new_link_names[linkindex], "temp"); - and_connect("new node"); - } - ret.state = ANALYSIS_NODE_START; - break; - case NODE: - typeStack[nodeindex] = selectedType(); - addNode(token_set_new_node_names[nodeindex], typeStack[nodeindex], typesetter); - if (!starGraph && nodeindex > 0) { - __addLink(token_set_new_node_names[nodeindex - 1], - token_set_new_node_names[nodeindex], - token_set_new_link_names[linkindex], typesetter); - and_connect(token_set_new_node_names[nodeindex]); - } - ret.state = ANALYSIS_NODE_START; - break; - case LINK: - linkindex++; - addNode("new node", selectedType(), "temp"); - if (!starGraph) { - __addLink(token_set_new_node_names[nodeindex - 1], "new node", token_set_new_link_names[linkindex], "temp"); - and_connect("new node"); - } - ret.state = ANALYSIS_LINK; - break; - } - - //EXTERNAL AND CONNECTION CHECKING - function and_connect(node) { - var verb; - for(var x=0;x 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 deleted file mode 100644 index 3fd4aecb..00000000 --- a/src/util.js +++ /dev/null @@ -1,72 +0,0 @@ -"use strict" - -define(function() { - - function assert(condition, message) { - if (false == condition) { - message = message || "Assertion failed"; - if (typeof Error !== "undefined") { - throw new Error(message); - } - throw message; // Fallback - } - } - - function set_from_array(a) { - var ret = {}; - for (var k = 0; k < a.length; ++k) { - ret[a[k]] = 1; - } - return ret; - } - - function set_from_object(o) { - var ret = {} - for ( var k in o) { - ret[k] = 1; - } - return ret; - } - - function set_diff(sa, sb) { - var ret = { - a_b : [], - b_a : [] - }; - var i; - for (i in sa) { - if (!(i in sb)) { - ret.a_b.push(i); - } - } - for (i in sb) { - if (!(i in sa)) { - ret.b_a.push(i); - } - } - return ret; - } - - function array_diff(aa, ab) { - var sa = set_from_array(aa); - var sb = set_from_array(ab); - return set_diff(sa, sb); - } - - // TODO: jquery BBQ: $.deparam.querystring().json; - function getParameterByName(name) { - name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); - var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), - results = regex.exec(location.search); - return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); - } - - return { - assert: assert, - set_from_array: set_from_array, - set_from_object: set_from_object, - set_diff: set_diff, - array_diff: array_diff, - getParameterByName: getParameterByName, - }; -}); diff --git a/src/view/completer.js b/src/view/completer.js deleted file mode 100644 index d2593321..00000000 --- a/src/view/completer.js +++ /dev/null @@ -1,229 +0,0 @@ -define( -['jquery', 'Bacon'], -function($, Bacon) { - -function unquoted(name) -{ - var start = 0, - end = name.length; - - if (name.length >= 1) { - if (name.charAt(0) == '"') { - start = 1; - if (name.length > 1 && name.charAt(name.length - 1) == '"') { - end = name.length - 1; - } - } - return name.substring(start, end); - } - return name; -} - -function setCaret(e, num) -{ - e.selectionStart = e.selectionEnd = num; -} - -var completer = (function (input_element, dropdown, base_config) { - var config = get_config(base_config), - dropdown_raw = dropdown[0], - options_bus = new Bacon.Bus(), - options = [], - selected_index = -1, - input_element_raw = input_element[0], - completion_start = 0, - completion_end = 0, - minimum_length = 1; - - // turn off the browser's autocomplete - input_element.attr('autocomplete', 'off'); - - //$('.ui-autocomplete').css('width', '10px'); - options_bus.onValue(function update_options(new_options) { - options = new_options; - }); - - input_element.keyup(function(e) { - var ret = undefined; - switch (e.keyCode) { - case 38: //UP - prev_option(); - ret = false; - break; - case 40: //DOWN - next_option(); - ret = false; - break; - case 27: // Escape - hide(); - ret = false; - break; - default: - // This catches cursor move due to keyboard events. no event for cursor movement itself - // below we catch cursor moves due to mouse click - oninput(input_element_raw.value, input_element_raw.selectionStart); - } - return ret; - }); - input_element.keydown(function(e) { - switch (e.keyCode) { - case 38: - case 40: - return false; - case 9: // Tab - if (config.hideOnTab) { - hide(); - } - break; - } - }); - - function get_config(base) { - return { - triggerStart: base && base.triggerStart || '#', - triggerEnd: base && base.triggerEnd || ' ', - hideOnTab: base && base.hasOwnProperty('hideOnTab') ? base.hideOnTab : true, - }; - } - - function completions(text) - { - var ret = [], - noquotes = unquoted(text.toLowerCase()); - - for (var name in options) { - if (name.toLowerCase().indexOf(noquotes) === 0) { - ret.push(name); - } - } - return ret; - } - - function show() { - if (dropdown.children().length > 0) { - dropdown.show(); - } - } - function hide() - { - dropdown.hide(); - } - - /*** - * #this is a # - * ^ - * - * #this is a #t - * ^ - * - * #this and #that then #he - * ^ - */ - function oninput(text, cursor) { - var hash = text.slice(0, cursor).lastIndexOf(config.triggerStart); - // TODO check if current completion has been invalidated - _invalidateSelection(); - hide(); - dropdown_raw.innerHTML = ""; // remove all elements - if (hash == -1 && config.triggerStart != ' ') { // space matches start of string too - return; - } - var space = text.slice(hash + 1).indexOf(config.triggerEnd); - space = space == -1 ? text.length : space; - if (space < cursor) { - return; - } - completion_start = hash + 1; - completion_end = space; - var string = text.slice(completion_start, completion_end); - if (string.length < minimum_length) { - return; - } - completions(string).forEach(function(name) { - var suggestion = $('
' + name + '
'); - suggestion.on('click', function(e) { - _applySuggestion(name); - input_element.focus(); - }); - dropdown.append(suggestion); - }); - show(); - } - - function _invalidateSelection() { - update_highlighting(-1); - } - - function _move_option(change, default_value) { - var next, - n = dropdown.children().length; - - if (n == 0) { - return; - } - show(); - if (selected_index == -1) { - next = default_value; - } else { - next = (selected_index + change) % n; - } - update_highlighting(next); - } - function next_option() { - _move_option(1, 0); - } - function prev_option() { - _move_option(dropdown.children().length - 1, dropdown.children().length - 1); - } - function _get_option(index) { - if (dropdown.children().length <= index) { - console.log('error: dropdown does not contain index ' + index + - ', it has ' + dropdown.children().length + ' elements'); - return ''; - } - var e = dropdown.children()[index], - s = e.innerText || e.textContent; - if (s.indexOf(' ') != -1) { - return '"' + s + '"'; - } - return s; - } - function _choice(i) { - return dropdown.children().eq(i); - } - function update_highlighting(new_index) { - if (selected_index != -1) { - _choice(selected_index).removeClass('selected'); - } - if (new_index != -1) { - _choice(new_index).addClass('selected'); - } - selected_index = new_index; - } - function _applySuggestion(str) { - var cur = input_element.val(), - start = cur.slice(0, completion_start) + str + ' '; - input_element.val(start + cur.slice(completion_end)); - setCaret(input_element, start.length); - oninput('', 0); - } - function handleEnter() { - if (selected_index == -1) { - return false; - } - _applySuggestion(_get_option(selected_index)); - return true; - } - - return { - options: options_bus, - oninput: oninput, - next_option: next_option, - prev_option: prev_option, - handleEnter: handleEnter, - }; -}); - -return completer; - -}); diff --git a/src/view/edge_info.js b/src/view/edge_info.js deleted file mode 100644 index d711e200..00000000 --- a/src/view/edge_info.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict" - -define(['view/internal'], -function(internal) { - -var delete_button = internal.edit_tab.get('edge', '#deleteedge'), - delete_callback = undefined; - -delete_button.on('click', function() { - if (delete_callback) { - delete_callback(); - } -}); - -function show(link) -{ - internal.edit_tab.show('edge'); - internal.edit_tab.get('edge', '#edgetitle').html(link.name); -} - -function hide() -{ - internal.edit_tab.hide(); -} - -function on_delete(f) -{ - delete_callback = f; -} - -return { - show: show, - hide: hide, - on_delete: on_delete, -}; -}); diff --git a/src/view/helpers.js b/src/view/helpers.js deleted file mode 100644 index ad11763a..00000000 --- a/src/view/helpers.js +++ /dev/null @@ -1,87 +0,0 @@ -"use strict" - -define(function() { -function customColor(type) { - var color; - switch (type) { - case "person": - color = '#FCB924'; //blue - break; - case "club": - color = '#ee3654'; //magenta - break; - case "skill": - color = '#fad900'; //yellow - break; - case "third-internship-proposal": - color = '#33c2e0'; //cyan - break; - case "internship": - color = '#ff8b11'; //orange - break; - case "interest": - color = '#8b3ab0'; //purple - break; - case "project": - color = "#40C200"; //green - break; - case "empty": - color = "#919095"; //mid-grey - break; - case "chainlink": - color = "#363636"; //dark-grey - break; - case "bubble": - color = "rgba(255,255,255,0.2)"; // white, 0.2 opaque - break; - default: - console.log('bug: unknown type ' + type); - color = '#d4d4d9'; //mid-light grey - break; - } - return color; -} - -function customSize(type) { - var size; - switch (type) { - case "person": - size = 12; - break; - case "club": - size = 12; - break; - case "skill": - size = 12; - break; - case "third-internship-proposal": - size = 12; - break; - case "internship": - size = 12; - break; - case "interest": - size = 12; - break; - case "empty": - size = 9; - break; - case "chainlink": - size = 8; - break; - case "bubble": - size = 180; - break; - default: - size = 9; - break; - } - return size; -} - - -return { - customSize: customSize, - customColor: customColor, -}; -}); diff --git a/src/view/internal.js b/src/view/internal.js deleted file mode 100644 index bc18bd75..00000000 --- a/src/view/internal.js +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 44f2fab8..00000000 --- a/src/view/node_info.js +++ /dev/null @@ -1,117 +0,0 @@ -define(['jquery', 'jquery-ui', 'view/helpers', 'view/internal'], -function($, _unused_jquery_ui, view_helpers, internal) { - -var d = null, - submit_callback = null, - delete_callback = null; - -function _get_form_data() { - return { - name: $('.info #editformname').val(), - type: $('.info #edittype').val(), - url: $('.info #editurl').val(), - status: $('.info #editstatus').val(), - startdate: $("#editstartdate").val(), - enddate: $("#editenddate").val(), - }; -} - -//internal.edit_tab.get('node', "#editbox").submit(function(e) { -// if (submit_callback) { -// return submit_callback(e, _get_form_data()); -// } -// console.log('bug: edit tab submit called with no callback set'); -// e.preventDefault(); -//}) - -//internal.edit_tab.get('node', "#deletenode").click(function(e) { -// if (delete_callback) { -// return delete_callback(e, _get_form_data()); -// } -// console.log('bug: edit tab delete called with no callback set'); -// e.preventDefault(); -//}); - -function show(d) { - var info = $('.info'), - f = false, - t = true, - visible = { - "third-internship-proposal": [t, t, t, f, f], - "chainlink": [f, f, f, f, f], - "skill": [f, f, f, f, t], - "interest": [f, f, f, f, t], - "_defaults": [f, f, f, f, t], - }, - fields = ["#status", "#startdate", "#enddate", "#desc", "#url"], - flags = visible.hasOwnProperty(d.type) ? visible[d.type] : visible._defaults, - i; - - internal.edit_tab.show('node'); - - for (i = 0 ; i < flags.length; ++i) { - var elem = info.find(fields[i]); - elem[flags[i] ? 'show' : 'hide'](); - } - - $('.info').attr('class', 'info'); - $('.info').addClass('type-' + d.type); // Add a class to distinguish types for css - - $('.info').find('#editformname').val(d.name); - $("#editenddate").datepicker({ - inline: true, - showOtherMonths: true, - dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], - }); - $("#editstartdate").datepicker({ - inline: true, - showOtherMonths: true, - dayNamesMin: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], - }); - - $('#editdescription').val(d.type); - - $('#edittype').val(d.type); - - $('#editurl').val(d.url); - - $('#editstatus').val(d.status); - - if (d.type === "third-internship-proposal") { - $('#editstartdate').val(d.start); - $('#editenddate').val(d.end); - } -} - -function hide() -{ - internal.edit_tab.hide(); -} - -function on_save(f) { - $('#edit-node-dialog__save').click(function(e) { - return f(e, _get_form_data()); - }); -} - -function on_delete(f) { - $('#edit-node-dialog__delete').click(function(e) { - return f(e, _get_form_data()); - }); -} - -function on_keyup(f) { - $('.info').keyup(function(e) { - return f(e, _get_form_data()); - }); -} - -return { - show: show, - hide: hide, - on_save: on_save, - on_delete: on_delete, - on_keyup: on_keyup, -}; - -}); diff --git a/src/view/selection.js b/src/view/selection.js deleted file mode 100644 index d51d1a4b..00000000 --- a/src/view/selection.js +++ /dev/null @@ -1,102 +0,0 @@ -define(['rz_core'], -function(rz_core) { - -function get_rz_core() -{ - // circular dependency on rz_core, so require.js cannot solve it. - if (rz_core === undefined) { - rz_core = require('rz_core'); - } - return rz_core; -} - -var selected_nodes = []; - -function byVisitors(node_selector, link_selector) { - var new_selected_nodes = get_rz_core().graph.findByVisitors(node_selector, link_selector); - - clear(); - connectedComponent(new_selected_nodes); -} - -function connectedComponent(nodes) { - var connected = get_rz_core().graph.getConnectedNodesAndLinks(nodes, 1), - i, - node, - link, - data; - - selected_nodes = nodes.map(function(x) { return x; }); - - for (i = 0 ; i < connected.nodes.length ; ++i) { - data = connected.nodes[i]; - node = data.node; - switch (data.type) { - case 'exit': - node.state = 'exit'; - break; - case 'enter': - node.state = 'enter'; - break; - }; - } - for (i = 0 ; i < connected.links.length ; ++i) { - data = connected.links[i]; - link = data.link; - switch (data.type) { - case 'exit': - link.state = 'exit'; - break; - case 'enter': - link.state = 'enter'; - break; - }; - } - nodes.forEach(function (n) { n.state = 'chosen'; }); -} - -var node_selected = function(node) { - return node.state == 'chosen' || node.state == 'enter' || node.state == 'exit' || node.state == 'selected' - || node.state == 'temp' || node.state == 'related'; -} - -var selected_class = function(node) { - return selected_nodes.length > 0 ? (node_selected(node) ? "selected" : "notselected") : ""; -} - -var clear = function() { - selected_nodes.length = 0; - get_rz_core().graph.setRegularState(); -} - -function arr_compare(a1, a2) -{ - if (a1.length != a2.length) { - return false; - } - for (var i = 0 ; i < a1.length ; ++i) { - if (a1[i] != a2[i]) { - return false; - } - } - return true; -} - -var update = function(nodes) { - var set = !arr_compare(nodes, selected_nodes); - clear(); - if (set) { - connectedComponent(nodes); - } -} - -return { - byVisitors: byVisitors, - connectedComponent: connectedComponent, - clear: clear, - update: update, - selected_class: selected_class, - node_selected: node_selected, -}; - -}); diff --git a/src/view/tab.js b/src/view/tab.js deleted file mode 100644 index 97ba1351..00000000 --- a/src/view/tab.js +++ /dev/null @@ -1,57 +0,0 @@ -"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 deleted file mode 100644 index 0a26c0a7..00000000 --- a/src/view/timeline.js +++ /dev/null @@ -1,180 +0,0 @@ -"use strict" - -define(['jquery', 'rz_core'], -function ($, rz_core) { - -function checkSwitch(checkswitch) { - - if (checkswitch.checked) { - vis.selectAll(".timeline").remove(); - $('.missingdates').fadeOut(300); - scrollValue = $('body').scrollLeft(); - - $('body').scrollLeft(0); - graphstate = "GRAPH"; - rz_core.update_view__graph(); - - $('.status').fadeOut(600); - - //boxedin=false; - - } else { - timelineTimer=0; - $('.missingdates').fadeIn(300); - - graph.recenterZoom(); - - $('body').scrollLeft(scrollValue); - - graphstate = "TIMELINE"; - - rz_core.update_view__graph(); - - $('.status').fadeIn(600); - - - initAxis(); - - //boxedin=true; - } - -} - -///For some reason JQuery's $('body').scroll never worked, so I found something else. -$('body').bind('DOMMouseScroll', function(e){ - if(graphstate==="TIMELINE"){ - if(e.originalEvent.detail !== 0) { - $('.overlay').hide(); - }else{ - $('.overlay').show(); - } - }else{ - return false; - } - }); - - //IE, Opera, Safari - $('body').bind('mousewheel', function(e){ - if(graphstate==="TIMELINE"){ - if(e.originalEvent.wheelDelta !== 0) { - $('.overlay').hide(); - }else{ - $('.overlay').show(); - var left = $('body').offset().left; - $('.overlay').scrollLeft(left); - } - }else{ - return false; - } - }); - -function initAxis() { - var bar_height = 20; - var row_height = bar_height + 10; - var vertical_padding = 150 - var bar_start_offset = 40; - var h = 15 * row_height + vertical_padding; - - - var min = deliverables[0].startdate; - var max = deliverables[0].enddate; - - for (var i = 0; i < deliverables.length; i++) { - var deliv = deliverables[i]; - if (min > deliv.startdate) min = deliv.startdate; - if (end < deliv.enddate) end = deliv.enddate; - } - min = new Date(min); ///with min - max = new Date(max); - console.log("min "+min+" max "+max); - - ///update interval - var timeDiff = Math.abs(max.getTime() - min.getTime()); - var diffDays = timeDiff / (1000 * 3600 * 24); - var w = Math.round(diffDays * 15 - 100); - graphinterval = w / diffDays; - - var svg=vis; - vis.attr("width",w); - - var paddingLeft = 150; - var paddingTop = 120; - - var xScale = d3.time.scale() - .domain([min, max]).nice() - .range([paddingLeft, w]); - - var xAxis = d3.svg.axis() - .scale(xScale) - .orient("bottom"); - - // Lines - var line = svg.append("g") - .selectAll("line") - .data(xScale.ticks(40)) - .enter().append("line") - .attr("x1", xScale) - .attr("x2", xScale) - .attr("y1", paddingTop + 30) - .attr("y2", h - 50) - .attr("class", "timeline") - .style("stroke", "#ccc"); - - var y = function (i) { - return i * row_height + paddingTop + bar_start_offset; - } - - var labelY = function (i) { - return i * row_height + paddingTop + bar_start_offset + 13; - } - - // Company bars - var bar = svg.selectAll("rect") - .data(function (d) { - return Math.random() * 5; - }); - - bar.enter().append("rect") - .attr("y", -100) - .attr("x", -1000) - .attr("width", 100) - .attr("height", bar_height) - .attr("class", "company-bar timeline") - .on("mouseover", function (d) { - d3.select(this).style("fill", "#F5AF00"); - getCompanyData(String(d.uid)) - }) - .on("mouseout", function () { - d3.select(this).style("fill", "#fc0"); - }); - - var label = svg.selectAll("text") - .data(deliverables, function (key) { - return key.id - });; - - label.enter().append("text") - .attr("class", "bar-label timeline") - // .attr("text-anchor","end") - .attr("x", paddingLeft - 10) - .attr("y", function (d, i) { - return labelY(i); - }) - .text(function (d) { - }); - - // Bottom Axis - var btmAxis = svg.append("g") - .attr("transform", "translate(0," + (h - 25) + ")") - .attr("class", "axis timeline") - .call(xAxis); - - // Top Axis - var topAxis = svg.append("g") - .attr("transform", "translate(0," + paddingTop + ")") - .attr("class", "axis timeline") - .call(xAxis); -} - -return {}; -}); diff --git a/src/view/view.js b/src/view/view.js deleted file mode 100644 index 95bfc09b..00000000 --- a/src/view/view.js +++ /dev/null @@ -1,12 +0,0 @@ -"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(); - }, -}; -}); -- cgit v1.3.1