diff options
| author | Alon Levy <alon@pobox.com> | 2014-12-16 17:37:42 +0200 |
|---|---|---|
| committer | Alon Levy <alon@pobox.com> | 2014-12-16 17:37:42 +0200 |
| commit | ade19de410e0a789a921ec1666b278b2a64176d6 (patch) | |
| tree | c4c54b106de93609ca7bb70a20fd4be696715dbe /src/client/model | |
| parent | c7d026b1d504323a8c61d51b726e5e8d2337fc4b (diff) | |
moving files around after repository merger
Diffstat (limited to 'src/client/model')
| -rw-r--r-- | src/client/model/core.js | 187 | ||||
| -rw-r--r-- | src/client/model/diff.js | 187 | ||||
| -rw-r--r-- | src/client/model/graph.js | 873 | ||||
| -rw-r--r-- | src/client/model/util.js | 134 |
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 |
