summaryrefslogtreecommitdiff
path: root/src/client/model
diff options
context:
space:
mode:
authorAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
committerAlon Levy <alon@pobox.com>2014-12-16 17:37:42 +0200
commitade19de410e0a789a921ec1666b278b2a64176d6 (patch)
treec4c54b106de93609ca7bb70a20fd4be696715dbe /src/client/model
parentc7d026b1d504323a8c61d51b726e5e8d2337fc4b (diff)
moving files around after repository merger
Diffstat (limited to 'src/client/model')
-rw-r--r--src/client/model/core.js187
-rw-r--r--src/client/model/diff.js187
-rw-r--r--src/client/model/graph.js873
-rw-r--r--src/client/model/util.js134
4 files changed, 1381 insertions, 0 deletions
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