/* This file is part of rhizi, a collaborative knowledge graph editor. Copyright (C) 2014-2015 Rhizi This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ "use strict"; define(['underscore', 'Bacon', 'consts', 'util', 'model/core', 'model/util', 'model/diff', 'rz_api_backend', 'rz_api_mesh', 'history', 'model/types'], function (_, Bacon, consts, util, model_core, model_util, model_diff, rz_api_backend, rz_api_mesh, history, model_types) { // aliases var all_attributes = model_types.all_attributes; var debug = false; function Graph(spec) { var id_to_node_map, id_to_link_map, id_to_link_id_set, diffBus = new Bacon.Bus(), activityBus = new Bacon.Bus(), cached_links, invalidate_links = true, cached_nodes, invalidate_nodes = true, temporary = spec.temporary, base = spec.base, server_pending_objects = [], filtered_types = {}; // set of node types not to show this.temporary = temporary; this.base = base; util.assert(temporary !== undefined && base !== undefined, "missing inputs"); clear(); // All operations done on the graph. When the server is used (i.e. always) this // bus contains the server events, not the user events (most of the time the same just with delay). this.diffBus = diffBus; // Same as diffBus, but includes operations done in the past, loaded when // the document is loaded (commit log) this.activityBus = activityBus; var links_forEach = function (f) { for (var link_key in id_to_link_map) { f(id_to_link_map[link_key]); } }; var nodes_forEach = function (f) { for (var node_key in id_to_node_map) { f(id_to_node_map[node_key]); } }; function key_count(obj) { return _.keys(obj).length; } var degree = function (node) { return key_count(id_to_link_id_set[node.id]); }; this.degree = degree; /** * 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.addTempNode = function(spec) { if (temporary) { return this.__addNode(spec); } }; var nodes_to_touched_links = function (node_id_set) { var touched_links = []; node_id_set.forEach(function (n_id) { var n = id_to_node_map[n_id]; links_forEach(function (link) { if ((link['__src'].equals(n)) || (link['__dst'].equals(n))) { // compare by id touched_links.push(link); } }); }); return touched_links.map(function(l){ return l.id; }); }; /** * * @param a topo_diff but that might be missing a few things, sanitize it first. * all sanitation should be idempotent, but probably isn't. * * NOTE: currently this function transmits only. Later we want to optimistically * first commit and then transmit. */ var commit_and_tx_diff__topo = function (topo_diff) { util.assert(temporary === false, "cannot be temporary"); $.merge(topo_diff.link_id_set_rm, nodes_to_touched_links(topo_diff.node_id_set_rm)); topo_diff.node_set_add = topo_diff.node_set_add .filter(function(n) { util.assert(undefined !== n.id, "undefined id in node in topo diff"); util.assert(undefined === server_pending_objects[n.id], "cache full at id"); return find_node__by_id(n.id) === null; }) .map(function(n_spec) { server_pending_objects[n_spec.id] = n_spec; return model_util.adapt_format_write_node(n_spec); }); topo_diff.link_set_add = topo_diff.link_set_add.map(function(l_spec) { util.assert(l_spec.id !== undefined, "undefined id in link in topo diff"); if (l_spec.source === undefined) { l_spec.source = l_spec.__src; } if (l_spec.target === undefined) { l_spec.target = l_spec.__dst; } if (l_spec.name == undefined) { // cannot have a zero length name, using name as label in neo4j l_spec.name = 'is'; } if (l_spec.__type === undefined) { l_spec.__type = l_spec.name; } server_pending_objects[l_spec.id] = l_spec; return model_util.adapt_format_write_link(l_spec); }); // filter already existing nodes now, after we conveniently used them // for name_to_node map topo_diff.node_set_add = topo_diff.node_set_add.filter(function(n) { return !hasNodeByName(n.name); }); rz_api_backend.commit_diff__topo(topo_diff, __commit_diff_ajax__topo); }; this.commit_and_tx_diff__topo = commit_and_tx_diff__topo; /** * Inner implementation * * @param notify whether or not a presenter notification will be sent, default = true */ function __addNode(spec) { var existing_node, node; if (undefined == spec.id) { existing_node = find_node__by_name(spec.name); if (existing_node){ return existing_node; } else { node = model_core.create_node__set_random_id(spec); if (debug) { 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.warn('__addNode: id collision: existing-node.id: \'' + existing_node.id); return existing_node; } util.assert(undefined != node.id, '__addNode: node id missing'); _node_add_helper(node); if (debug) { console.log('__addNode: node added: id: ' + node.id + ' state ' + node.state); } return node; } this.__addNode = __addNode; var _node_remove_helper = function (node_id) { util.assert(node_id, "missing node id"); delete id_to_node_map[node_id]; delete id_to_link_id_set[node_id]; invalidate_nodes = true; }; var _node_add_helper = function (node) { util.assert(node.id, "missing node id"); id_to_node_map[node.id] = node; id_to_link_id_set[node.id] = []; invalidate_nodes = true; }; var _link_remove_helper = function (link_id) { var link = id_to_link_map[link_id], src_id = link.__src.id, dst_id = link.__dst.id; util.assert(link_id, "missing link id"); util.assert(link, "non existent link"); delete id_to_link_id_set[src_id][dst_id]; delete id_to_link_id_set[dst_id][src_id]; delete id_to_link_map[link_id]; util.assert(id_to_link_map[link_id] === undefined, "delete failed?!"); invalidate_links = true; }; var _link_add_helper = function (link) { var src_id = link.__src.id, dst_id = link.__dst.id; util.assert(link.id, "missing link id"); id_to_link_map[link.id] = link; // link's nodes may not belong to this graph, check first - we add them if required to the id_to_link_id_set only if (id_to_link_id_set[src_id] === undefined) { id_to_link_id_set[src_id] = []; } if (id_to_link_id_set[dst_id] === undefined) { id_to_link_id_set[dst_id] = []; } id_to_link_id_set[src_id][dst_id] = 1; id_to_link_id_set[dst_id][src_id] = 1; invalidate_links = true; return link; }; var _remove_node_set = function(node_id_set) { node_id_set.forEach(function (id) { if (undefined === id_to_node_map[id]) { console.log("warning: server returned an id we don't have " + id); return; } _node_remove_helper(id); console.log('_remove_node_set: ' + id); }); }; this._remove_node_set = _remove_node_set; function calc_neighbours() { return _.reduce(get_links(), function(d, link) { d[link.__src.id].src.push(link); d[link.__dst.id].dst.push(link); return d; }, _.object(_.map(get_nodes(), "id"), get_nodes().map(function (n) { return {node: n, src: [], dst: []}; }) )); } /** * Visitation constants for neighbourhood and shortest paths computation. */ var kind_exit = 1, kind_enter = 2, kind_selected = 4; function kind_to_string(kind) { switch (kind) { case kind_exit: return 'exit'; case kind_enter: return 'enter'; case kind_selected: return 'selected'; default: // TODO: add css for both return 'exit'; } } function BFS(node_id) { var neighbours = calc_neighbours(), queue = [node_id], start_id, node_ids = get_node_ids(), V = _.object(node_ids, _.map(node_ids, function (id) { return {node_id: id, distance: Infinity, prev: {}}; })), ret = {}; V[node_id].distance = 0; while ((start_id = queue.shift()) !== undefined) { var src_ids = _.pluck(_.pluck(neighbours[start_id].src, "__dst"), "id"), dst_ids = _.pluck(_.pluck(neighbours[start_id].dst, "__src"), "id"), n_ids = src_ids.concat(dst_ids); _.each(n_ids, function(next_id) { var distance = V[start_id].distance + 1; if (V[next_id].distance >= distance) { V[next_id].distance = distance; V[next_id].prev[start_id] = true; queue.push(next_id); } }); } _.each(_.keys(V), function (k) { if (V[k].distance !== Infinity) { ret[k] = V[k]; } }); return ret; } this.BFS = BFS; /** * pairs_symmetric * * cb will be called for every pair in the input list but only in the order * lower_index, maybe_higher_index * where lower_index <= maybe_higher_index (i.e. diagonal is covered). * * i.e. for |list| = N, (N + 1) * N / 2 calls are made */ function pairs_symmetric(list, cb) { var i, j, N = list.length; for (i = 0 ; i < N; ++i) { for (j = i; j < N ; ++j) { cb(list[i], list[j]); } } } /** * @sources - list of nodes * * returns all nodes in the shortest paths between all sources. * * returns same dictionary as neighbourhood. */ function shortest_paths(sources) { function make_status(node, distance, prev_nodes) { return {node: node, distances: distance || 0, prev_nodes: prev_nodes || {}}; } var ids = _.pluck(sources, 'id'), bfs = _.object(ids, _.map(ids, BFS)), nodes = {}; function append_paths(bfs, start_id) { var queue = [bfs[start_id]], next, next_id; while ((next = queue.shift()) !== undefined) { next_id = next.node_id; if (nodes[next_id] === undefined) { nodes[next_id] = {node_id: next_id, sources: {}}; } _.each(_.keys(next.prev), function (p) { nodes[next_id].sources[p] = true; queue.push(bfs[p]); }); } } pairs_symmetric(ids, function (one, two) { if (bfs[one][two] !== undefined && bfs[one][two].distance === Infinity) { return; } append_paths(bfs[one], two); }); return { 'nodes': _.values(nodes), 'links': [] }; } this.shortest_paths = shortest_paths; /** * * neighbourhood * * @start - list of starting nodes * @d - radius of neighbours * * NOTE: Doesn't handle inter graph links * NOTE: return doesn't include original nodes * * @return - { * 'nodes': [{ * node: node, * kind: kind, * sources: {node_id: true} * }] * 'links: [{link: link, kind: kind}] * } * * kind: exit/enter * * TODO: implement for d !== 1 * */ this.neighbourhood = function(start, d) { var ret = {'nodes':[], 'links':[]}; function addNode(node) { if (start.filter(function (n) { return n.id == node.id; }).length == 1) { return; } ret.nodes.push(node); } function get_name(node) { // XXX: using lowercase name comparison instead of id because nodes may be stale return node.name.toLowerCase(); } if (start === undefined) { console.log('neighbourhood: bug: called with undefined node'); return ret; } if (d > 1) { console.log('neighbourhood: bug: not implemented for d == ' + d); } if (d === 0) { // 0 depth is empty group of nodes and links return ret; } d = d || 1; if (start.length === undefined) { console.log('neighbourhood: expected array'); return ret; } function make_status(kind, node) { return {node: node, kind: kind, links: [], depth: Infinity, sources: {}}; } var nodes = get_nodes(), links = get_links(), neighbours = calc_neighbours(), visited = _.object(_.map(start, get_name), _.map(start, _.partial(make_status, kind_selected))); function visit(source, link, getter, kind, depth) { var node = getter(link), name = get_name(node), data = visited[name]; if (data === undefined) { data = visited[name] = make_status(0, node); } data.kind |= kind; data.links.push({link: link, kind: kind}); data.depth = Math.min(data.depth, depth); data.sources[source.id] = true; return data; } _.each(start, function (node) { var N = neighbours[node.id]; _.each(N.src, function (link) { visit(node, link, function (link) { return link.__dst; }, kind_enter); }); _.each(N.dst, function (link) { visit(node, link, function (link) { return link.__src; }, kind_exit); }); }); _.values(visited).forEach(function (data) { var node = data.node, kind = data.kind, links = data.links; if ((kind & kind_selected) === kind_selected) { return; } ret.nodes.push({type: kind_to_string(kind), node: node, sources: data.sources}); _.each(links, function (data) { ret.links.push({link: data.link, kind: kind_to_string(kind)}); }); }); 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) { var state_nodes = find_nodes__by_state(state); var state_links = find_links__by_state(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}; }; function __addLink(link) { var trimmed_name = link.name.trim(); util.assert(link instanceof model_core.Link); if (link.name.length != trimmed_name.length) { console.log('bug: __addLink with name containing spaces - removing before sending to server'); } link.name = trimmed_name; var existing_link = findLink(link.__src.id, link.__dst.id, link.name); if (undefined == existing_link) { existing_link = _link_add_helper(link); } else { existing_link.name = link.name; existing_link.state = link.state; } return existing_link; } // FIXME: good idea to add API based on constructor parameter? if (temporary) { this.addTempLink = function (link) { return __addLink(link); }; } this.update_link = function(link, new_link_spec, on_success, on_error) { util.assert(link instanceof model_core.Link); // TODO - fake api for client only (debug, demo, ui work) if (!rz_config.backend_enabled) return; var attr_diff = model_diff.new_attr_diff(); for (var key in new_link_spec) { attr_diff.add_link_attr_write(link.id, key, new_link_spec[key]); } var on_ajax_success = function(attr_diff_spec) { var attr_diff = model_util.adapt_format_read_diff__attr(attr_diff_spec), id_to_link_map = attr_diff.id_to_link_map, key, l_id = link.id; // original node id util.assert(id_to_link_map && id_to_link_map[l_id], "bad return value from ajax"); var ret_link = id_to_link_map[l_id]; for (key in ret_link['__attr_write']){ link[key] = ret_link['__attr_write'][key]; } for (key in ret_link['__attr_remove']){ delete link[key]; } // TODO: handle NAK: add problem emblem to link if (on_success !== undefined) { on_success(); } diffBus.push(attr_diff); }; var on_ajax_error = function(){ console.log('error with commit to server: danger robinson!'); }; rz_api_backend.commit_diff__attr(attr_diff, on_ajax_success, on_ajax_error); }; function layout_x_key(layout_name) { return 'layouts_' + layout_name + '_x'; } this.layout_x_key = layout_x_key; function layout_y_key(layout_name) { return 'layouts_' + layout_name + '_y'; } this.layout_y_key = layout_y_key; function layout_fixed_key(layout_name) { return 'layouts_' + layout_name + '_fixed'; } this.layout_fixed_key = layout_fixed_key; function close_to(a, b, eps) { return Math.abs(a - b) < eps; } var eps = 0.001; /** * Do an attribute commit with x, y for the current layout */ this.nodes__store_layout_positions = function(layout_name, node_ids) { // TODO: fix when attribute diff supports nested keys to use: // layout..{x,y} var x_key = layout_x_key(layout_name), y_key = layout_y_key(layout_name), fixed_key = layout_fixed_key(layout_name), changes = 0; if (node_ids && node_ids.forEach) { node_ids = node_ids.filter(function (node_id) { return id_to_node_map[node_id] !== undefined; }); } else { node_ids = _.keys(id_to_node_map); } // commit x, y to layout node_ids.forEach(function (node_id) { var node = id_to_node_map[node_id], db_x = node[x_key], db_y = node[y_key], x = node.x, y = node.y; if (close_to(db_x, x, eps) && close_to(db_y, y, eps)) { return; } node[x_key] = node.x; node[y_key] = node.y; changes += 1; }); if (changes === 0) { return; } console.log('sending ' + changes + ' nodes position for layout ' + layout_name); // commit all current nodes nodes__commit_attributes(node_ids, function (node) { var d = {}; d[x_key] = node.x; d[y_key] = node.y; d[fixed_key] = node.fixed; return d; }); }; function nodes__commit_attributes(node_ids, getter) { var attr_diff = model_diff.new_attr_diff(); node_ids.forEach(function (node_id) { var node = id_to_node_map[node_id], d = getter(node); _.keys(d).forEach(function (k) { attr_diff.add_node_attr_write(node.id, k, d[k]); }); }); var on_ajax_success = function() { }; var on_ajax_error = function(){ console.log('error with commit nodes properties to server'); }; rz_api_backend.commit_diff__attr(attr_diff, on_ajax_success, on_ajax_error); } this.update_node = function(node, new_node_spec) { util.assert(node instanceof model_core.Node); // TODO - fake api for client only (debug, demo, ui work) if (!rz_config.backend_enabled) { return; } if (new_node_spec.name !== undefined && node.name !== new_node_spec.name){ /* * handle name update collision: suggest removal first */ var n_eq_name = find_node__by_name(new_node_spec.name); if (null !== n_eq_name && n_eq_name !== node) { if (window.confirm('really merge ' + node.name + ' into ' + n_eq_name.name + '?')) { nodes__merge([n_eq_name.id, node.id]); } } 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(attr_diff_spec){ var attr_diff = model_util.adapt_format_read_diff__attr(attr_diff_spec), id_to_node_map = attr_diff.id_to_node_map, key, n_id = node.id; // original node id util.assert(id_to_node_map && id_to_node_map[n_id], "bad return value from ajax"); var ret_node = id_to_node_map[n_id]; for (key in ret_node.__attr_write){ node[key] = ret_node.__attr_write[key]; } for (key in ret_node.__attr_remove){ delete node[key]; } diffBus.push(attr_diff); }; var on_ajax_error = function(){ console.log('error with commit to server: danger robinson!'); }; rz_api_backend.commit_diff__attr(attr_diff, on_ajax_success, on_ajax_error); }; var update_node = this.update_node; this.editNameByName = function(old_name, new_name) { var node = find_node__by_name(old_name); if (node === undefined) { console.log('editNameByName: error: cannot find node with name ' + old_name); return; } this.editName(node.id, new_name); }; this.editName = function(id, new_name) { var n_eq_name = find_node__by_name(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; } util.assert(temporary, 'editName should now only be used on temporary graphs'); util.assert(n_eq_name === undefined); n_eq_id.name = new_name; }; /** * editType: * * @return true if type changed */ this.editType = function(id, newtype) { return this._editProperty(id, 'type', newtype); }; function new_attr_diff_prop_value(id, prop, value) { var diff = model_diff.new_attr_diff(); diff.add_node_attr_write(id, prop, value); return diff; } this._editProperty = function(id, prop, value) { var n = find_node__by_id(id), local = find_node__by_id(id, false); if ((n === undefined)) { return false; } if (local === null) { return base._editProperty(id, prop, value); } if (temporary) { n[prop] = value; diffBus.push(new_attr_diff_prop_value(id, prop, value)); } else { // FIXME: should not do a server roundtrip, should keep this data local // and part of the temporary graph, and send it on user enter in a single commit. // The current implementation is just a quick way to get sorta the same outcome. // it misses atomicity (since we create a commit for every tab click on an existing node), // and responsiveness (since there is a roundtrip to the server and it isn't client side) var props = {}; props[prop] = value; update_node(n, props); } return true; }; this.links__delete = function(link_ids) { var topo_diff = model_diff.new_topo_diff({link_id_set_rm: link_ids}); this.commit_and_tx_diff__topo(topo_diff); }; this.nodes__delete = function(node_ids) { var topo_diff = model_diff.new_topo_diff({node_id_set_rm: node_ids}); this.commit_and_tx_diff__topo(topo_diff); }; /** * links the first node in the list to the rest of the list. */ var nodes__link_fan = function(node_ids) { util.assert(node_ids.length > 1); // strictly speaking we can also treat 1 as correct usage var src_id = node_ids[0], src_node = find_node__by_id(src_id), added_links = node_ids.slice(1).map(function (tgt_id) { return model_core.create_link__set_random_id(src_node, find_node__by_id(tgt_id), {name: consts.EMPTY_LINK_NAME}); }); commit_and_tx_diff__topo(model_diff.new_topo_diff({link_set_add: added_links})); }; this.nodes__link_fan = nodes__link_fan; var nodes__merge = function(node_ids) { util.assert(node_ids.length > 1); // strictly speaking we can also treat 1 as correct usage var merged = _.rest(node_ids); var merge_node_id = node_ids[0]; var merge_node = find_node__by_id(merge_node_id); var topo_diff; util.assert(merge_node != null); var added_links = _.flatten(_.map(merged, function (node_id) { var src_links = find_link__by_src_id(node_id) .filter(function (src_link) { return src_link.__dst.id !== merge_node_id; }) .map(function (src_link) { return model_core.create_link__set_random_id(merge_node, src_link.__dst, { name: src_link.name }); }); var dst_links = find_link__by_dst_id(node_id) .filter(function (dst_link) { return dst_link.__src.id !== merge_node_id; }) .map(function (dst_link) { return model_core.create_link__set_random_id(dst_link.__src, merge_node, { name: dst_link.name }); }); return _.union(src_links, dst_links); })); topo_diff = model_diff.new_topo_diff({ link_set_add: added_links, node_id_set_rm: merged}); commit_and_tx_diff__topo(topo_diff); }; this.nodes__merge = nodes__merge; var _remove_link_set = function(link_id_set) { link_id_set.forEach(function (id) { var link = id_to_link_map[id]; if (undefined === link) { console.log("warning: server returned an id we don't have " + id); return; } _link_remove_helper(id); console.log('_remove_link_set: ' + id); }); }; this._remove_link_set = _remove_link_set; this.nodes_rm = function(state) { var node_ids = get_nodes().filter(function (n) { return n.state == state; }) .map(function (n) { return n.id; }), topo_diff = model_diff.new_topo_diff({ node_id_set_rm : node_ids }); this.commit_and_tx_diff__topo(topo_diff); }; var findLink = function(src_id, dst_id, name) { var link_key, link; for (link_key in id_to_link_map) { link = id_to_link_map[link_key]; if (link.__src.id === src_id && link.__dst.id === dst_id) { return link; } } return undefined; }; var find_links__by_nodes = function(nodes) { var ids = util.set_from_array(_.pluck(nodes, "id")); return get_links().filter(function (link) { return ids[link.__src.id] && ids[link.__dst.id]; }); }; this.find_links__by_nodes = find_links__by_nodes; var find_links__by_state = function(state) { var foundLinks = []; links_forEach(function (link) { if (link.state == state) { foundLinks.push(link); } }); return foundLinks; }; var compareNames = function(name1, name2) { return name1.toLowerCase() === name2.toLowerCase(); }; var hasNodeByName = function(name, state) { return get_nodes().filter(function (n) { return compareNames(n.name, name) && (undefined === state || n.state === state); }).length > 0; }; this.hasNodeByName = hasNodeByName; /** * return node whose id matches the given id or undefined if no node was found */ var find_node__by_id = function(id, recursive) { // default to recursion recursive = recursive === undefined ? true : recursive; if (recursive && base) { var base_node = base.find_node__by_id(id, recursive); if (base_node) { return base_node; } } return id_to_node_map[id] || null; }; this.find_node__by_id = find_node__by_id; var find_nodes__by_id = function(ids, recursive) { return _.map(ids, function (id) { return find_node__by_id(id, recursive); }); }; this.find_nodes__by_id = find_nodes__by_id; /** * @param filter: must return true in order for node to be included in the returned set */ var find_nodes__by_filter = function(filter) { var ret = []; _.values(id_to_node_map).map(function(n) { if (true == filter(n)){ ret.push(n); } }); return ret; }; /** * @param id unique id of link * @return Link with given id */ var find_link__by_id = function(id, recursive) { // default to recursion recursive = recursive === undefined ? true : recursive; if (recursive && base) { var base_link = base.find_link__by_id(id, recursive); if (base_link) { return base_link; } } return id_to_link_map[id] || null; }; this.find_link__by_id = find_link__by_id; /** * @param id of source node * @return array of links whose source node is id * FIXME: none O(E) implementation (used by merge) */ var find_link__by_src_id = function(src_id) { return _.filter(get_links(), function (link) { return link.__src.id == src_id; }); }; this.find_link__by_src_id = find_link__by_src_id; /** * @param id of destination node * @return array of links whose destination node is id * FIXME: none O(E) implementation (used by merge) */ var find_link__by_dst_id = function(dst_id) { return _.filter(get_links(), function (link) { return link.__dst.id == dst_id; }); }; this.find_link__by_dst_id = find_link__by_dst_id; var find_node__by_name = function(name, recursive) { // default to recursion recursive = recursive === undefined ? true : recursive; if (recursive && base) { var node = base.find_node__by_name(name, true); if (node !== null) { return node; } } for (var k in id_to_node_map) { if (compareNames(id_to_node_map[k].name, name)) { return id_to_node_map[k]; } } return null; }; this.find_node__by_name = find_node__by_name; var find_nodes__by_state = function(state) { var foundNodes = []; nodes_forEach(function (node) { if (node.state === state) { foundNodes.push(node); } }); return foundNodes; }; function clear(push_diff) { push_diff = push_diff === undefined ? true : push_diff; id_to_node_map = {}; id_to_link_map = {}; id_to_link_id_set = {}; invalidate_links = true; invalidate_nodes = true; if (push_diff) { diffBus.push({}); // FIXME: better value } } this.clear = clear; function empty() { // FIXME: O(|nodes|+|links|) return get_nodes().length == 0 && get_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); }; function on_backend__node_add(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'); return n_spec; } function on_backend__link_add(l_spec) { var src_id = l_spec.__src_id, dst_id = l_spec.__dst_id, l_ptr = model_util.adapt_format_read_link_ptr(l_spec); util.assert(undefined !== l_ptr.id, 'load_from_backend: l_ptr missing id'); util.assert(undefined !== src_id, 'load_from_backend: link missing __src_id'); util.assert(undefined !== dst_id, 'load_from_backend: link missing __dst_id'); // cleanup & reuse as link_spec delete l_ptr.__src_id; delete l_ptr.__dst_id; var link_spec = l_ptr; link_spec.__src = find_node__by_id(src_id); link_spec.__dst = find_node__by_id(dst_id); if (null === link_spec.__src) { util.log_error("src_id not found: " + src_id); return null; } if (null === link_spec.__dst) { util.log_error("dst_id not found: " + dst_id); return null; } return link_spec; } function __commit_diff_ajax__clone(clone) { var topo = clone[0], commits = clone[1].reverse(), // [!] received in new to old order, need them reversed node_specs = topo.node_set_add.map(on_backend__node_add), nodes = _add_node_set(node_specs), link_specs = topo.link_set_add.map(on_backend__link_add).filter(function (link_spec) { return link_spec !== null; }), links = _add_link_set(link_specs); commits.forEach(function (commit) { activityBus.push(commit); }); diffBus.push({node_set_add: nodes, link_set_add: links}); } function __commit_diff_ajax__topo(diff) { diff.node_set_add = diff.node_id_set_add.map(_get_server_pending); diff.link_set_add = diff.link_id_set_add.map(_get_server_pending); commit_diff__topo(diff); } function _get_server_pending(id) { // FIXME: should track cache var spec; util.assert(undefined !== server_pending_objects[id]); spec = server_pending_objects[id]; delete server_pending_objects[id]; return spec; } function _add_node_set(node_specs) { return node_specs.map(function (node_spec) { return __addNode(node_spec); }); } function _add_link_set(link_specs) { return link_specs.map(function (link_spec) { // resolve link ptr var src = (undefined !== link_spec.__src && null !== link_spec.__src && (find_node__by_id(link_spec.__src.id) || link_spec.__src)) || find_node__by_id(link_spec.__src_id), dst = (undefined !== link_spec.__dst && null !== link_spec.__dst && (find_node__by_id(link_spec.__dst.id) || link_spec.__dst)) || find_node__by_id(link_spec.__dst_id), link = model_core.create_link_from_spec(src, dst, link_spec); return __addLink(link); }); } /* * Inputs are specs, not raw - after adaptation from the on wire format. * * FIXME: use a different object? different properties in the same object? */ function commit_diff__topo(diff) { _add_node_set(diff.node_set_add); _add_link_set(diff.link_set_add); // done under protest _remove_link_set(diff.link_id_set_rm); _remove_node_set(diff.node_id_set_rm); diffBus.push(diff); } this.commit_diff__topo = commit_diff__topo; /** * Apply a Attr_Diff: * - commit diff to the local graph instanse * - emit a diffBus event * * This function should not trigger remote transmission of diff object */ function commit_diff__attr(attr_diff) { var total_count_d = 0, total_count_w = 0, count_n = 0; util.assert(model_diff.is_attr_diff(attr_diff), 'commit_diff__attr: argument type != Attr_Diff'); // process nodes attr_diff.for_each_node(function(n_id, n_attr_diff) { var attr_key, node = id_to_node_map[n_id]; if (undefined === node) { console.warn('commit_diff__attr: incoming attr diff for non-existing node, discarding'); return; } // apply attr writes: node var count_w = 0; for (attr_key in n_attr_diff.__attr_write) { var attr_value = n_attr_diff.__attr_write[attr_key]; node[attr_key] = attr_value; // write each new attr update count_w = count_w + 1; } // apply attr removals: node var count_d = 0; for (attr_key in n_attr_diff.__attr_remove) { delete node[attr_key]; // apply each attr removal count_d = count_d + 1; } if (count_w > 0 || count_d > 0) { count_n += 1; } total_count_w += count_w; total_count_d += count_d; }); console.log('commit_diff__attr: nodes: ' + count_n + ', writes: ' + total_count_w + ', removals: ' + total_count_d); // process links attr_diff.for_each_link(function(l_id, n_attr_diff) { var attr_key, link = id_to_link_map[l_id]; if (undefined === link) { console.warn('commit_diff__attr: incoming attr diff for non-existing link, discarding'); return; } // apply attr writes: link var count_w = 0; for (attr_key in n_attr_diff.__attr_write) { var attr_value = n_attr_diff.__attr_write[attr_key]; link[attr_key] = attr_value; // write each new attr update count_w = count_w + 1; } // apply attr removals: link var count_d = 0; for (attr_key in n_attr_diff.__attr_remove) { delete link[attr_key]; // apply each attr removal count_d = count_d + 1; } console.log('commit_diff__attr: l_id: \'' + l_id + '\', write-count: ' + count_w + ', rm-count: ' + count_d); }); diffBus.push(attr_diff); } this.commit_diff__attr = commit_diff__attr; /** * 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, on_error) { function on_success_wrapper(clone) { __commit_diff_ajax__clone(clone); undefined != on_success && on_success(); } rz_api_backend.rzdoc_clone(on_success_wrapper, on_error); } this.load_from_backend = load_from_backend; var new_topo_diff__from_nodes_links = function (nodes, links) { var diff, node_by_id = {}, old_id_to_new_id = {}; diff = model_diff.new_topo_diff(); diff.node_set_add = nodes.map(function(ext_spec) { var node_spec = { name: ext_spec.name ? ext_spec.name : ext_spec.id, type: ext_spec.type, x: ext_spec.x, y: ext_spec.y }, node; all_attributes.forEach(function (attr) { node_spec[attr] = ext_spec[attr]; }); if (ext_spec.start) { node_spec.start = new Date(ext_spec.start); } if (ext_spec.end) { node_spec.end = new Date(ext_spec.end); } node = model_core.create_node__set_random_id(node_spec); old_id_to_new_id[ext_spec.id] = node.id, node_by_id[node.id] = node; return node; }); diff.link_set_add = links.map(function (link_spec) { var src = node_by_id[old_id_to_new_id[link_spec.__src]], dst = node_by_id[old_id_to_new_id[link_spec.__dst]], link = model_core.create_link__set_random_id(src, dst, { name: link_spec.name, state: 'perm' // FIXME: this is meaningless now with graph separation }); link.__src_id = src.id; link.__dst_id = dst.id; return link; }); return diff; }; this.new_topo_diff__from_nodes_links = new_topo_diff__from_nodes_links; this.load_from_json = function(json) { var data = JSON.parse(json); if (data == null) { console.log('load callback: no data to load'); return; } // FIXME: prompt for replace/merge; now defaulting to merge commit_and_tx_diff__topo(new_topo_diff__from_nodes_links(data.nodes, data.links)); }; this.save_to_json = function() { var d = {"nodes":[], "links":[]}, nodes = get_nodes(), links = get_links(); for (var i = 0 ; i < nodes.length ; i++) { var node = nodes[i], node_dict = {id: node.id, x: node.x, y: node.y}; all_attributes.forEach(function (attr) { node_dict[attr] = node[attr]; }); d['nodes'].push(node_dict); } 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() { if (cached_nodes === undefined || invalidate_nodes) { cached_nodes = _.filter(id_to_node_map, function (node, node_id) { return filtered_types[node.type] === undefined; }); invalidate_nodes = false; } return cached_nodes; }; this.nodes = get_nodes; var get_node_ids = function() { return _.keys(id_to_node_map); }; this.get_node_ids = get_node_ids; var get_links = function() { if (cached_links === undefined || invalidate_links) { cached_links = _.filter(id_to_link_map, function (link, link_id) { return filtered_types[link.__src.type] === undefined && filtered_types[link.__dst.type] === undefined; }); invalidate_links = false; } return cached_links; }; this.links = get_links; this.find__by_visitors = function(node_visitor, link_visitor) { var nodes = get_nodes(), links = get_links(), selected_nodes, selected_links; selected_nodes = node_visitor ? nodes.filter(node_visitor) : []; selected_links = link_visitor ? links.filter(link_visitor) : []; return {nodes: selected_nodes, links: selected_links}; }; function markRelated(names) { removeRelated(); nodes_forEach(function (node) { names.forEach(function (name) { if (compareNames(node.name, name)) { node.state = 'related'; } }); }); } this.markRelated = markRelated; function removeRelated() { // FIXME: related should use separate variable, not overload 'state' (bad bad bad) nodes_forEach(function (node) { if (node.state === 'related') { node.state = 'perm'; } }); } this.removeRelated = removeRelated; this.node__set_filtered_types = function (new_filtered_types) { filtered_types = new_filtered_types; invalidate_links = true; invalidate_nodes = true; }; } function is_node(item) { return item.__src === undefined; } function is_link(item) { return item.__src !== undefined; } return { Graph: Graph, is_node: is_node, is_link: is_link }; });