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/rz_core.js | |
| parent | c7d026b1d504323a8c61d51b726e5e8d2337fc4b (diff) | |
moving files around after repository merger
Diffstat (limited to 'src/client/rz_core.js')
| -rw-r--r-- | src/client/rz_core.js | 756 |
1 files changed, 756 insertions, 0 deletions
diff --git a/src/client/rz_core.js b/src/client/rz_core.js new file mode 100644 index 00000000..7ec19692 --- /dev/null +++ b/src/client/rz_core.js @@ -0,0 +1,756 @@ +"use strict" + +define(['jquery', 'd3', 'consts', 'rz_bus', 'util', 'model/graph', 'model/core', 'view/helpers', 'view/view', 'rz_observer', 'view/selection', 'rz_config'], +function($, d3, consts, rz_bus, util, model_graph, model_core, view_helpers, view, rz_observer, selection, rz_config) { + +var addednodes = [], + vis, + graphinterval = 0, + timeline_timer = 0, + deliverables = [], + circle, // <-- should not be module globals. + scrollValue = 0, + graph, + drag, + force; + +// "CSS" for SVG elements. Reused for editing elements. +var node_text_dx = 15, + node_text_dy = '.30em', + svg_input_fo_node_x = node_text_dx, + svg_input_fo_node_y = '-.70em', + svg_input_fo_height = '30px'; + +/** + * svgInput - creates an embedded input element under a given + * + * edit_node(@sibling, @node) + * edit_link(@sibling, @link) + */ +var svgInput = (function() { + var measure_node = $('#measure-node')[0], + measure_link = $('#measure-link')[0], + original_element, + is_link; + + function appendForeignElementInputWithID(base, elemid, width, height) + { + var input = document.createElement('input'), + body = document.createElement('body'), + fo = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); + + body.appendChild(input); + + fo.setAttribute('height', height || svg_input_fo_height); + fo.style.pointerEvents = 'none'; + input.style.pointerEvents = 'all'; + fo.appendChild(body); + base.appendChild(fo); + input.setAttribute('id', elemid); + return input; + } + + function measure(text) + { + var span; + + span = is_link ? measure_link : measure_node; + span.innerHTML = text; + return span.getBoundingClientRect().width; // $().width() works too + } + + function onkeydown(e) { + var ret = undefined, + jelement = createOrGetSvgInput(), + element = jelement[0], + newname = jelement.val(), + fo = createOrGetSvgInputFO(), + d; + + if (element != this) { + console.log('unexpected editname_on_keypress this should be the svg-input element'); + } + + if (e.which == 13 || e.which == 27) { + ret = false; + d = jelement.data().d; + if (e.which == 13 && newname != d.name) { + if (d.hasOwnProperty('__src')) { + graph.editLink(d.__src.id, d.__dst.id, newname); + } else { + graph.editName(d.id, newname); + } + rz_bus.names.push([newname]); + update_view__graph(true); + } + hide(); + } + rz_bus.ui_key.push({where: consts.KEYSTROKE_WHERE_EDIT_NODE, keys: [e.which]}); + return ret; + }; + + function resize_measure(e) { + resize(measure($(e.target).val()) + 30); + } + + function resize(new_width) { + var svg_input = createOrGetSvgInput(), + fo = createOrGetSvgInputFO(); + + svg_input.css('width', new_width); + fo.attr('width', new_width); + } + + // FIXME: element being deleted. Some delete is legit - removal of related element. Some isn't (a click). + // Instead of investigating (time constraint) reparenting as sibling, and introducing + // this function. Cost of creation of element is negligble, it's just ugly.. + function createOrGetSvgInput() + { + var svg_input_name = 'svg-input', + svg_input_selector = '#' + svg_input_name, + svg_input = $(svg_input_selector); + + if (svg_input.length == 0) { + console.log('creating new svg-input'); + svg_input = $(appendForeignElementInputWithID(vis[0][0], svg_input_name)); + svg_input.on('keydown', onkeydown); + svg_input.bind('change keypress', resize_measure); + } + return svg_input; + } + + function createOrGetSvgInputFO() + { + return createOrGetSvgInput().parent().parent(); + } + + /* + * @param e visual node element + * @param n node model object + */ + function enable(e, n) { + var oldname = n.name, + svg_input = createOrGetSvgInput(), + fo = createOrGetSvgInputFO(); + + is_link = n.hasOwnProperty('__src'); + + e.parentNode.appendChild(fo[0]); // This will unparent from the old parent + if (is_link) { + fo.attr('transform', e.getAttribute('transform')); + // XXX links set the text-anchor middle attribute. no idea how to do that + fo.attr('x', -$(e).width() / 2); + fo.attr('y', -$(e).height() / 2 - 3); // XXX This minus 3 is only kinda ok. + fo.attr('class', 'svg-input-fo-link'); + } else { + fo.attr('x', svg_input_fo_node_x); + fo.attr('y', svg_input_fo_node_y); + fo.attr('transform', null); + fo.attr('class', 'svg-input-fo-node'); + } + // Set width correctly + resize(measure(oldname) + 30); + fo.show(); + svg_input.val(oldname); + svg_input.data().d = n; + svg_input.focus(); + if (original_element) { + original_element.show(); + } + original_element = $(e); + original_element.hide(); + // TODO: set cursor to correct location in text + } + + function hide() { + createOrGetSvgInputFO().hide(); + if (original_element && original_element.show) { + original_element.show(); + } + } + + return { + enable: enable, + hide: hide, + }; +}()); + + + +function recenterZoom() { + vis.attr("transform", "translate(0,0)scale(1)"); +} + +// zoom or drag +var zoomInProgress = false; + +var initDrawingArea = function () { + + function zoom() { + zoomInProgress = true; + vis.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); + d3.event.sourceEvent.stopPropagation(); + } + + function dragstarted(d) { + d3.event.sourceEvent.stopPropagation(); + d3.select(this).classed("dragging", true); + d.dragstart = {clientX:d3.event.sourceEvent.clientX, clientY:d3.event.sourceEvent.clientY}; + force.stop(); + } + + function dragged(d) { + d.x = d3.event.x; + d.y = d3.event.y; + tick(); + } + + function dragended(d) { + d3.select(this).classed("dragging", false); + d3.select(this).classed("fixed", true); // TODO: this is broken since we override all the classes. Need to switch to class addition/removal (i.e. use classed for everything) or set class in one location (so here just set a value on the node, not the element) + if (d.dragstart.clientX - d3.event.sourceEvent.clientX != 0 || + d.dragstart.clientY - d3.event.sourceEvent.clientY != 0) { + tick(); + force.resume(); + } + } + + graph = new model_graph.Graph(); + + var user_id = $('#user_id'), + user = user_id.text(); + + if (user_id.length > 0) { + console.log('found user ID: \'' + user + '\''); + graph.set_user(user); + } + + var el = document.body; + vis = d3.select(el).append("svg:svg") + .attr('id', 'canvas_d3') + .attr("width", '100%') + .attr("height", '100%') + .attr("pointer-events", "all") + .append("g") + .attr("class", "zoom"); + + d3.select(el).select("svg").append("svg:defs") + .data(["end"]) // Different link/path types can be defined here + .append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 22) + .attr("refY", -1.5) + .attr("markerWidth", 4) + .attr("markerHeight", 4) + .attr("orient", "auto") + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + /* + * init zoom behavior + */ + var zoom_obj = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); + zoom_obj(d3.select('#canvas_d3')) + d3.select("svg").on("dblclick.zoom", null); // disable zoom on double click + + $('svg').click(svg_click_handler); + + // SVG rendering order is last rendered on top, so to make sure + // all links are below the nodes we group them under a single g + vis.append("g").attr("id", "link-group"); + vis.append("g").attr("id", "selected-link-group"); + + drag = d3.behavior.drag() + .origin(function(d) { return d; }) + .on("dragstart", dragstarted) + .on("drag", dragged) + .on("dragend", dragended); + + // $('#canvas_d3').dblclick(canvas_handler_dblclick); - see #138 + if (rz_config.backend_enabled){ + graph.load_from_backend( function(){ + update_view__graph(false); + }); + } +} + +function init_force_layout(){ + var el = document.body; + var w = $(el).innerWidth(), + h = $(el).innerHeight(); + + force = d3.layout.force() + .distance(120) + .gravity(0.12) + .charge(-1800) + .size([w, h]) + .on("tick", tick) + .start(); +} + +initDrawingArea(); +init_force_layout(); + +/** + * find the visual element counterpart of a given model object. This relies on + * the visual element having an id attribute equal to the object's id. + * + * @return null if visual element is not found + */ +function locate_visual_element(model_obj){ + var id_sel = $('#' + model_obj.id); + if (0 == id_sel.length){ + console.warn('unable to find visual element for model object: object id: ' + model_obj.id.toString()) + return null; + } + return id_sel[0]; +} + +/** + * add node on canvas double click + */ +function canvas_handler_dblclick(){ + var n = model_core.create_node__set_random_id(); + n.name = ''; // will be set by user + + graph.addNode(n); + update_view__graph(); + + var n_ve = locate_visual_element(n); // locate visual element + + var on_slowdown_cb = function(){ + svgInput.enable($(n_ve).find('.nodetext'), n); + observer.disconnect(); + } + var mutation_handler = rz_observer.new_Mutation_Handler__on_dxy_slowdown(on_slowdown_cb); + var observer = rz_observer.new_MutationObserver(mutation_handler); + mutation_handler.on_slowdown_threshold_reached; + + observer.observe(n_ve, { + subtree: false, + childList : false, + attributes: true, + attributeOldValue : true, + }); +} + +/** + * update view: graph + */ +function update_view__graph(no_relayout) { + var node, + link, + link_g, + linktext, + nodetext, + unselected_link_group = document.querySelector('#link-group'), + selected_link_group = document.querySelector('#selected-link-group'); + + link = vis.selectAll("g.link") + .data(graph.links(), function(d) { return d.id; }); + + link_g = link.enter().append('g') + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr('class', 'link graph') + + link_g.append("path") + .attr("class", function(d) { + return d.state + ' link graph'; + }) + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr("marker-end", "url(#end)"); + + // second path for larger click area + link_g.append("path") + .attr("class", "ghostlink") + .on("click", function(d, i) { + if (zoomInProgress) { + // don't disable zoomInProgress, it will be disabled by the svg_click_handler + // after this events bubbles to the svg element + return; + } + var that = this, + src = this.link.__src, + dst = this.link.__dst; + + view.edge_info.on_delete(function () { + graph.removeLink(that.link); + update_view__graph(true); + view.edge_info.hide(); + }); + view.edge_info.show(d); + selection.update([src, dst]); + update_view__graph(true); + }); + + link.attr("class", function(d, i){ + var temp_and = (d.name && d.name.replace(/ /g,"")=="and" && d.state==="temp") ? "temp_and" : ""; + + return ["graph link", temp_and, selection.selected_class(d)].join(' '); + }); + + link.selectAll('path.link') + .attr('class', function(d) { + return [d.state, selection.selected_class(d), "link graph"].join(' '); + }); + + link.exit().remove(); + + vis.selectAll('.ghostlink') + .data(graph.links()) + .each(function (d) { + this.link = d; + }); + + linktext = vis.selectAll(".linklabel") + .data(graph.links(), function(d) { return d.id; }); + linktext.enter() + .append("text") + .attr('id', function(d){ return d.id; }) // append link id to enable data->visual mapping + .attr("class", function(d) { + return ["linklabel graph", selection.selected_class(d)].join(' '); + }) + .attr("text-anchor", "middle") + .on("click", function(d, i) { + if (d.state !== "temp") { + svgInput.enable(this, d); + } + }); + + linktext + .text(function(d) { + var name = d.name || ""; + if (!(d.__dst.state === "temp" || + d.__src.state === "chosen" || d.__dst.state === "chosen")) { + return ""; + } + if (name.length < 25 || d.__src.state === "chosen" || + d.__dst.state === "chosen" || d.state==="temp") { + return name; + } else { + return name.substring(0, 14) + "..."; + } + }); + + linktext.exit().remove(); + + node = vis.selectAll(".node") + .data(graph.nodes(), function(d) { + return d.id; + }); + + var nodeEnter = node.enter() + .append("g") + .attr('id', function(d){ return d.id; }) // append node id to enable data->visual mapping + .attr('visibility', 'hidden') // made visible on first tick + .call(drag); + + // reorder nodes so selected are last, and so rendered last, and so on top. + (function () { + var ontop = [], + bubble; + + node.each(function (d) { + this.node = d; + }) + .attr('class', function(d) { + if (selection.node_selected(d)) { + if (d.type == 'bubble') { + bubble = this; + } else { + ontop.push(this); + } + } + return ['node', selection.selected_class(d)].join(' '); + }); + if (bubble === undefined) { + // nothing to do if there is no bubble + return; + } + function reparent(new_parent, element) { + if (element.parentNode == new_parent) { + return; + } + new_parent.appendChild(element); + } + // move link to correct group + // O(|links|*|ontop|) + link.each(function (d) { + if (ontop.some(function (node) { + var d_node = node.node; + return d.__src == d_node || d.__dst == d_node; + })) + { + reparent(selected_link_group, this); + } else { + reparent(unselected_link_group, this); + } + }); + linktext.each(function (d) { + if (selection.node_selected(d)) { + ontop.push(this); + } + }); + function moveToEnd(e) { + e.parentNode.appendChild(e); + } + moveToEnd(bubble); + ontop.reverse().forEach(function (e) { + moveToEnd(e); + }); + var count_links = function() { + return selected_link_group.childElementCount + unselected_link_group.childElementCount; + }; + // put back on top link group on top + selected_link_group.parentNode.insertBefore(selected_link_group, bubble.nextSibling); + })(); + + nodetext = nodeEnter.insert("text") + .attr("class", "nodetext graph") + .attr("dx", node_text_dx) + .attr("dy", node_text_dy) + .on("click", function(d, i) { + if (d3.event.defaultPrevented) { + // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on + return; + } + if (d.state !== "temp") { + svgInput.enable(this, d); + selection.update([d]); + showNodeInfo(this.parentNode.node, i); + } + d3.event.stopPropagation(); + }); + + node.select('g.node text') + .text(function(d) { + if (!d.name) { + return d.type == 'bubble' ? "" : "_"; + } + if (d.state === "temp" || d.state === 'chosen' + || d.state === "enter" || d.state === "exit") { + return d.name; + } else { + if (d.name.length < 28) { + return d.name; + } else { + return d.name.substring(0, 25) + "..."; + } + } + }); + + circle = nodeEnter.insert("circle"); + node.select('g.node circle') + .attr("class", function(d) { + return d.type + " " + d.state + " circle graph"; + }) + .attr("r", function(d) { + return view_helpers.customSize(d.type) - 2; + }) + .on("click", function(d, i) { + if (d3.event.defaultPrevented) { + // drag happened, ignore click https://github.com/mbostock/d3/wiki/Drag-Behavior#on + return; + } + d3.event.stopPropagation(); + selection.update([d]); + if(d.state !== "temp") { + showNodeInfo(d, i); + } + }); + circle.append("svg:image") + .attr("class", "status graph") + .attr('x', -7) + .attr('y', -8) + .attr('width', 15) + .attr('height', 15) + .attr("xlink:href", function(d) { + switch (d.status) { + case "done": + return "res/img/check.png"; + break; + case "current": + return "res/img/wait.png"; + break; + case "waiting": + return "res/img/cross.png"; + break; + } + }); + + node.exit().remove(); + + //update deliverables + deliverables = []; + var nodes = graph.nodes(); + for (var i = 0; i < nodes.length; i++) { + var current = nodes[i]; + if (current.type === "third-internship-proposal") { + deliverables.push({ + "id": nodes[i].id, + "startdate": nodes[i].start, + "enddate": nodes[i].end + }); + } + //Do something + } + + force.nodes(graph.nodes()) + .links(graph.links()) + + if (no_relayout) { + // XXX If we are stopped we need to update the text of the links at least, + // and this is the simplest way + tick(); + } else { + force.alpha(0.1).start(); + } +} + + + +var debug_print = function(message) { + var element = $(".debug"); + if (element.length == 1) { + element.html(message); + } else { + console.log(message); + } +} + +function check_for_nan(x) { + if (Number.isNaN(x)) { + console.log('nan problem'); + force.stop(); + } + return Number.isNaN(x); +} + +var newnodes=1; +function tick(e) { + //console.log(e); + //$(".debug").html(force.alpha()); + var node = vis.selectAll(".node") + .data(force.nodes(), function(d) { + return d.id; + }); + var link = vis.selectAll("path.link") + .data(graph.links(), function(d) { + return d.id; + }); + var linktext = vis.selectAll(".linklabel").data(graph.links()); + + function transform(d) { + if (check_for_nan(d.x) || check_for_nan(d.y)) { + return; + } + return "translate(" + d.x + "," + d.y + ")"; + } + + //circles animation + var tempcounter = 0, + temptotal = graph.nodes().filter(function(d){ + return d.state === "temp" && d.type !== "chainlink" && d.type !== "bubble"; + }).length; + if (temptotal !== newnodes) { + newnodes += temptotal / 15 / (newnodes * newnodes); + } + newnodes = Math.max(1, Math.min(newnodes, temptotal)); + graph.nodes().forEach(function(d, i) { + var r, a; + if (d.state === "temp") { + tempcounter++; + if (d.type==="chainlink" || d.type==="bubble") { + d.x = window.innerWidth / 2; + d.y = window.innerHeight / 2; + } else { + r = 60 + newnodes * 20; + a = -Math.PI + Math.PI * 2 * (tempcounter-1) / newnodes + 0.3; + d.x = window.innerWidth / 2 + r * Math.cos(a); + d.y = window.innerHeight / 2 + r * Math.sin(a); + } + check_for_nan(d.x); + check_for_nan(d.y); + } + }); + + link.attr("d", function(d, i) { + var d_val, + ghost; + + var dx = d.__dst.x - d.__src.x, + dy = d.__dst.y - d.__src.y, + dr = Math.sqrt(dx * dx + dy * dy); + d_val = "M" + d.__src.x + "," + d.__src.y + "L" + d.__dst.x + "," + d.__dst.y; + // update ghostlink position + ghost = $(this.nextElementSibling); + ghost.attr("d", d_val); + return d_val; + }); + + linktext.attr("transform", function(d) { + return "translate(" + (d.__src.x + d.__dst.x) / 2 + "," + (d.__src.y + d.__dst.y) / 2 + ")"; + }); + + node.attr("transform", transform); + + // After initial placement we can make the nodes visible. + //links.attr('visibility', 'visible'); + node.attr('visibility', 'visible'); +} + +function showNodeInfo(d, i) { + view.node_info.on_save(function(e, form_data) { + + graph.update_node(d, form_data, function(){ + var old_type = d.type, + new_type = form_data.type; + + if (new_type != old_type) { + view.node_info.show(d); + } + + view.node_info.hide(); + update_view__graph(true); + }); + + return false; + }); + + view.node_info.on_delete(function() { + if (confirm('This node and all its connections will be deleted, are you sure?')) { + graph.removeNode(d.id); + update_view__graph(false); + view.node_info.hide(); + } + }); + + view.node_info.show(d); +} + +function svg_click_handler(e) { + if (zoomInProgress) { + zoomInProgress = false; + return; + } + if (e.originalEvent.target.nodeName != 'svg') { + return; + } + svgInput.hide(); + selection.clear(); + view.hide(); + update_view__graph(true); +} + +return { + graph: graph, + force: force, + load_from_json: function(result) { + graph.load_from_json(result); + recenterZoom(); + update_view__graph(false); + }, + update_view__graph : update_view__graph, +} +}); /* close define call */ |
