/** @odoo-module **/ import { Vec2, BRect, BEllipse } from './vec2'; // This serves as the end of an edge when creating a link function EdgeEnd (pos_x, pos_y) { this.x = pos_x; this.y = pos_y; this.get_pos = function () { return new Vec2(this.x, this.y); }; } // A close button, // if entity_type == "node": // GraphNode.destruction_callback(entity) is called where entity is a node. // If it returns true the node and all connected edges are destroyed. // if entity_type == "edge": // GraphEdge.destruction_callback(entity) is called where entity is an edge // If it returns true the edge is destroyed // pos_x,pos_y is the relative position of the close button to the // entity position (entity.get_pos()) function CloseButton (graph, entity, entity_type, pos_x, pos_y) { var self = this; var visible = false; var close_button_radius = graph.style.close_button_radius || 8; var close_circle = graph.r.circle( entity.get_pos().x + pos_x, entity.get_pos().y + pos_y, close_button_radius); // The outer gray circle close_circle.attr( { 'opacity': 0, 'fill': graph.style.close_button_color || "black", 'cursor': 'pointer', 'stroke': 'none', } ); close_circle.transform(graph.get_transform()); graph.set_scrolling(close_circle); // The 'x' inside the circle var close_label = graph.r.text( entity.get_pos().x + pos_x, entity.get_pos().y + pos_y, "x"); close_label.attr( { 'fill': graph.style.close_button_x_color || "white", 'font-size': close_button_radius, 'cursor': 'pointer', } ); close_label.transform(graph.get_transform()); graph.set_scrolling(close_label); // The dummy_circle is used to catch events, and avoid hover in/out // madness between the 'x' and the button var dummy_circle = graph.r.circle( entity.get_pos().x + pos_x, entity.get_pos().y + pos_y, close_button_radius); dummy_circle.attr( { 'opacity':1, 'fill': 'transparent', 'stroke':'none', 'cursor':'pointer', } ); dummy_circle.transform(graph.get_transform()); graph.set_scrolling(dummy_circle); this.get_pos = function () { return entity.get_pos().add_xy(pos_x, pos_y); }; this.update_pos = function () { var pos = self.get_pos(); close_circle.attr({'cx':pos.x, 'cy':pos.y}); dummy_circle.attr({'cx':pos.x, 'cy':pos.y}); close_label.attr({'x':pos.x, 'y':pos.y}); }; function hover_in () { if (!visible) { return; } close_circle.animate( {'r': close_button_radius * 1.5}, 300, 'elastic'); dummy_circle.animate( {'r': close_button_radius * 1.5}, 300, 'elastic'); } function hover_out () { if (!visible) { return; } close_circle.animate( {'r': close_button_radius}, 400, 'linear'); dummy_circle.animate( {'r': close_button_radius}, 400, 'linear'); } dummy_circle.hover(hover_in, hover_out); close_circle.hover(hover_in, hover_out); close_label.hover(hover_in, hover_out); function click_action () { if (!visible) { return; } close_circle.attr({'r': close_button_radius * 2}); dummy_circle.attr({'r': close_button_radius * 2}); close_circle.animate({'r': close_button_radius}, 400, 'linear'); dummy_circle.animate({'r': close_button_radius}, 400, 'linear'); if (entity_type === "node") { // eslint-disable-next-line no-use-before-define Promise.resolve(GraphNode.destruction_callback(entity)).then( function () { entity.remove(); } ); } else if (entity_type === "edge") { // eslint-disable-next-line no-use-before-define Promise.resolve(GraphEdge.destruction_callback(entity)).then( function () { entity.remove(); } ); } } dummy_circle.click(click_action); close_circle.click(click_action); close_label.click(click_action); this.show = function () { if (!visible) { close_circle.animate({'opacity': 1}, 100, 'linear'); close_label.animate({'opacity': 1}, 100, 'linear'); visible = true; } }; this.hide = function () { if (visible) { close_circle.animate({'opacity': 0}, 100, 'linear'); close_label.animate({'opacity': 0}, 100, 'linear'); visible = false; } }; // Destroy this object and remove it from the graph this.remove = function () { if (visible) { visible = false; close_circle.animate({'opacity': 0}, 100, 'linear'); close_label.animate( {'opacity': 0}, 100, 'linear', self.remove); } else { close_circle.remove(); close_label.remove(); dummy_circle.remove(); } }; } // Connectors are start and end point of edge creation drags. function Connector (graph, node, pos_x, pos_y) { var visible = false; var conn_circle = graph.r.circle( node.get_pos().x + pos_x, node.get_pos().y + pos_y, 4); conn_circle.attr( { 'opacity': 0, 'fill': graph.style.node_outline_color, 'stroke': 'none', } ); conn_circle.transform(graph.get_transform()); graph.set_scrolling(conn_circle); var self = this; this.update_pos = function () { conn_circle.attr( { 'cx':node.get_pos().x + pos_x, 'cy':node.get_pos().y + pos_y, } ); }; this.get_pos = function () { return new node.get_pos().add_xy(pos_x, pos_y); }; this.remove = function () { conn_circle.remove(); }; function hover_in () { if (!visible) { return; } conn_circle.animate({'r': 8}, 300, 'elastic'); if (graph.creating_edge) { graph.target_node = node; conn_circle.animate( { 'fill': graph.style.connector_active_color, 'stroke': graph.style.node_outline_color, 'stroke-width': graph.style.node_selected_width, }, 100, 'linear' ); } } function hover_out () { if (!visible) { return; } conn_circle.animate( { 'r': graph.style.connector_radius, 'fill':graph.style.node_outline_color, 'stroke':'none', }, 400, 'linear' ); graph.target_node = null; } conn_circle.hover(hover_in, hover_out); var drag_down = function () { if (!visible) { return; } self.ox = conn_circle.attr("cx"); self.oy = conn_circle.attr("cy"); self.edge_start = new EdgeEnd(self.ox, self.oy); self.edge_end = new EdgeEnd(self.ox, self.oy); // eslint-disable-next-line no-use-before-define self.edge_tmp = new GraphEdge( graph, '', self.edge_start, self.edge_end, true); graph.creating_edge = true; }; var drag_move = function (dx, dy) { if (!visible) { return; } self.edge_end.x = self.ox + dx; self.edge_end.y = self.oy + dy; self.edge_tmp.update(); }; var drag_up = function () { if (!visible) { return; } graph.creating_edge = false; self.edge_tmp.remove(); if (graph.target_node) { // eslint-disable-next-line no-use-before-define var edge_prop = GraphEdge.creation_callback( node, graph.target_node); if (edge_prop) { // eslint-disable-next-line no-use-before-define var new_edge = new GraphEdge( graph, edge_prop.label, node, graph.target_node); // eslint-disable-next-line no-use-before-define GraphEdge.new_edge_callback(new_edge); } } }; conn_circle.drag(drag_move, drag_down, drag_up); function show () { if (!visible) { conn_circle.animate({'opacity': 1}, 100, 'linear'); visible = true; } } function hide () { if (visible) { conn_circle.animate({'opacity': 0}, 100, 'linear'); visible = false; } } this.show = show; this.hide = hide; } // Creates a new graph on raphael document r. // style is a dictionary containing the style definitions // viewport (optional) is the dom element representing the viewport of the // graph. // It is used to prevent scrolling to scroll the graph outside the // viewport. function Graph (r, style, viewport, is_readonly) { var self = this; // List of all nodes in the graph var nodes = []; // List of all edges in the graph var edges = []; // The graph[n1.uid][n2.uid] -> list of all edges from n1 to n2 var graph = {}; // The links[n.uid] -> list of all edges from or to n var links = {}; // All nodes and edges have an uid used to order their display when // they are curved var uid = 1; // The selected entity (node or edge) var selected_entity = null; this.readonly = is_readonly; // True if we are dragging a new edge onto a node self.creating_edge = false; // This holds the target node when creating an edge and hovering a // connector self.target_node = null; // The raphael instance self.r = r; // Definition of the colors, spacing, fonts, ... used by the elements self.style = style; // Global translation coordinate var tr_x = 0, tr_y = 0; var background = r.rect(0, 0, '100%', '100%').attr( {'fill': 'white', 'stroke': 'none', 'opacity': 0, 'cursor': 'move'} ); // Return the global transform of the scene this.get_transform = function () { return "T" + tr_x + "," + tr_y; }; // Translate every element of the graph except the background. // Elements inserted in the graph after a translate_all() must manually // apply transformation via get_transform() var translate_all = function (dx, dy) { tr_x += dx; tr_y += dy; var tstr = self.get_transform(); r.forEach(function (el) { if (el !== background) { el.transform(tstr); } }); }; this.translate_graph = translate_all; // Returns {minx, miny, maxx, maxy}, // the translated bounds containing all nodes var get_bounds = function () { var minx = Number.MAX_VALUE; var miny = Number.MAX_VALUE; var maxx = Number.MIN_VALUE; var maxy = Number.MIN_VALUE; for (var i = 0; i < nodes.length; i++) { var pos = nodes[i].get_pos(); minx = Math.min(minx, pos.x); miny = Math.min(miny, pos.y); maxx = Math.max(maxx, pos.x); maxy = Math.max(maxy, pos.y); } minx = minx - style.node_size_x / 2 + tr_x; miny = miny - style.node_size_y / 2 + tr_y; maxx = maxx + style.node_size_x / 2 + tr_x; maxy = maxy + style.node_size_y / 2 + tr_y; return {minx:minx, miny:miny, maxx:maxx, maxy:maxy}; }; // Returns false if the translation dx,dy of the viewport // hides the graph (with optional margin) var translation_respects_viewport = function (dx, dy, margin) { if (!viewport) { return true; } var margin_norm = margin || 0; var b = get_bounds(); var width = viewport.offsetWidth; var height = viewport.offsetHeight; if (dy < 0 && b.maxy + dy < margin_norm || dy > 0 && b.miny + dy > height - margin_norm || dx < 0 && b.maxx + dx < margin_norm || dx > 0 && b.minx + dx > width - margin_norm) { return false; } return true; }; // Adds a mousewheel event callback to raph_element that scrolls the // viewport this.set_scrolling = function (raph_element) { $(raph_element.node).bind('mousewheel', function (event, delta) { var dy = delta * 20; if ( translation_respects_viewport(0, dy, style.viewport_margin) ) { translate_all(0, dy); } }); }; // eslint-disable-next-line init-declarations var px, py; // Graph translation when background is dragged var bg_drag_down = function () { px = py = 0; }; var bg_drag_move = function (x, y) { var dx = x - px; var dy = y - py; px = x; py = y; if (translation_respects_viewport(dx, dy, style.viewport_margin)) { translate_all(dx, dy); } }; // eslint-disable-next-line no-empty-function var bg_drag_up = function () {}; background.drag(bg_drag_move, bg_drag_down, bg_drag_up); this.set_scrolling(background); // Adds a node to the graph and sets its uid. this.add_node = function (n) { nodes.push(n); n.uid = uid++; }; // Return the list of all nodes in the graph this.get_node_list = function () { return nodes; }; // Adds an edge to the graph and sets its uid this.add_edge = function (n1, n2, e) { edges.push(e); e.uid = uid++; if (!graph[n1.uid]) { graph[n1.uid] = {}; } if (!graph[n1.uid][n2.uid]) { graph[n1.uid][n2.uid] = []; } if (!links[n1.uid]) { links[n1.uid] = []; } if (!links[n2.uid]) { links[n2.uid] = []; } graph[n1.uid][n2.uid].push(e); links[n1.uid].push(e); if (n1 !== n2) { links[n2.uid].push(e); } }; // Removes an edge from the graph this.remove_edge = function (edge) { edges = _.without(edges, edge); var n1 = edge.get_start(); var n2 = edge.get_end(); links[n1.uid] = _.without(links[n1.uid], edge); links[n2.uid] = _.without(links[n2.uid], edge); graph[n1.uid][n2.uid] = _.without(graph[n1.uid][n2.uid], edge); if (selected_entity === edge) { selected_entity = null; } }; // Removes a node and all connected edges from the graph this.remove_node = function (node) { var linked_edges = self.get_linked_edge_list(node); for (var i = 0; i < linked_edges.length; i++) { linked_edges[i].remove(); } nodes = _.without(nodes, node); if (selected_entity === node) { selected_entity = null; } }; // Return the list of edges from n1 to n2 this.get_edge_list = function (n1, n2) { var list = []; if (!graph[n1.uid]) { return list; } if (!graph[n1.uid][n2.uid]) { return list; } return graph[n1.uid][n2.uid]; }; // Returns the list of all edge connected to n this.get_linked_edge_list = function (n) { if (!links[n.uid]) { return []; } return links[n.uid]; }; // Return a curvature index so that all edges connecting n1, n2 have // different curvatures this.get_edge_curvature = function (n1, n2, e) { var el_12 = this.get_edge_list(n1, n2); var c12 = el_12.length; var el_21 = this.get_edge_list(n2, n1); var c21 = el_21.length; if (c12 + c21 === 1) { // Only one edge return 0; } var index = 0; for (var i = 0; i < c12; i++) { if (el_12[i].uid < e.uid) { index++; } } if (c21 === 0) { // All edges in the same direction return index - (c12-1) / 2.0; } return index + 0.5; }; // Returns the angle in degrees of the edge loop. // We do not support more than 8 loops on one node this.get_loop_angle = function (n, e) { // The 8 angles where we can put the loops var slots = []; for (var angle = 0; angle < 360; angle += 45) { slots.push(Vec2.new_polar_deg(1, angle)); } // We assign to each slot a score. The higher the score, // the closer it is to other edges. var links_ = this.get_linked_edge_list(n); for (var i = 0; i < links_.length; i++) { var edge = links_[i]; if (!edge.is_loop || edge.is_loop()) { continue; } var end = edge.get_end(); if (end === n) { end = edge.get_start(); } var dir = end.get_pos().sub(n.get_pos()).normalize(); for (var s = 0; s < slots.length; s++) { var score = slots[s].dot(dir); if (score < 0) { score = -0.2 * Math.pow(score, 2); } else { score = Math.pow(score, 2); } // eslint-disable-next-line no-negated-condition if (!slots[s].score) { slots[s].score = score; } else { slots[s].score += score; } } } // We want the loops with lower uid to get // the slots with the lower score slots.sort(function (a, b) { return a.score < b.score ? -1: 1; }); var index = 0; for (var j = 0; j < links_.length; j++) { edge = links_[j]; if (!edge.is_loop || !edge.is_loop()) { continue; } if (edge.uid < e.uid) { index++; } } index %= slots.length; return slots[index].angle_deg(); }; // Selects a node or an edge and deselects everything else // Do not perform this if graph is readonly this.select = function (entity) { if (selected_entity && !this.readonly) { if (selected_entity === entity) { return; } if (selected_entity.set_not_selected) { selected_entity.set_not_selected(); } selected_entity = null; } selected_entity = entity; if (entity && entity.set_selected && !this.readonly) { entity.set_selected(); } }; } // Creates a new Graph Node on Raphael document r, // centered on [pos_x,pos_y], with label 'label', // and of type 'circle' or 'rect', and of color 'color' function GraphNode (graph, pos_x, pos_y, label, type, color, color_label, highlight_node_color) { var self = this; var r = graph.r; var sy = graph.style.node_size_y; var sx = graph.style.node_size_x; var node_fig = null; var selected = false; this.connectors = []; this.close_button = null; this.uid = 0; graph.add_node(this); if (type === 'circle') { node_fig = r.ellipse(pos_x, pos_y, sx / 2, sy / 2); } else { node_fig = r.rect(pos_x - sx / 2, pos_y - sy / 2, sx, sy); } node_fig.attr( { 'fill': highlight_node_color || color, 'stroke': highlight_node_color ? "#FFD500" :graph.style.node_outline_color, 'stroke-width': highlight_node_color ? "5px" :graph.style.node_outline_width, 'cursor': 'pointer', } ); node_fig.transform(graph.get_transform()); graph.set_scrolling(node_fig); var node_label = r.text(pos_x, pos_y, label); node_label.attr( { 'fill': color_label, 'font-size': graph.style.node_label_font_size, 'cursor': 'pointer', } ); node_label.transform(graph.get_transform()); graph.set_scrolling(node_label); // Redraws all edges linked to this node var update_linked_edges = function () { var edges = graph.get_linked_edge_list(self); for (var i = 0; i < edges.length; i++) { edges[i].update(); } }; // Sets the center position of the node var set_pos = function (pos) { if (type === 'circle') { node_fig.attr({'cx': pos.x, 'cy': pos.y}); } else { node_fig.attr({'x': pos.x - sx / 2, 'y': pos.y - sy / 2}); } node_label.attr({'x': pos.x, 'y': pos.y}); for (var i = 0; i < self.connectors.length; i++) { self.connectors[i].update_pos(); } if (self.close_button) { self.close_button.update_pos(); } update_linked_edges(); }; // Returns the figure used to draw the node var get_fig = function () { return node_fig; }; // Returns the center coordinates var get_pos = function () { if (type === 'circle') { return new Vec2(node_fig.attr('cx'), node_fig.attr('cy')); } return new Vec2( node_fig.attr('x') + sx / 2, node_fig.attr('y') + sy / 2 ); }; // Return the label string var get_label = function () { return node_label.attr("text"); }; // Sets the label string var set_label = function (text) { node_label.attr({'text': text}); }; var get_bound = function () { if (type === 'circle') { return new BEllipse(get_pos().x, get_pos().y, sx / 2, sy / 2); } return BRect.new_centered(get_pos().x, get_pos().y, sx, sy); }; // Selects this node and deselects all other nodes var set_selected = function () { if (!selected) { selected = true; node_fig.attr( { 'stroke': graph.style.node_selected_color, 'stroke-width': graph.style.node_selected_width, } ); if (!self.close_button) { self.close_button = new CloseButton( graph, self, "node", sx / 2, - sy / 2); self.close_button.show(); } for (var i = 0; i < self.connectors.length; i++) { self.connectors[i].show(); } } }; // Deselect this node var set_not_selected = function () { if (selected) { node_fig.animate( { 'stroke': graph.style.node_outline_color, 'stroke-width': graph.style.node_outline_width, }, 100, 'linear' ); if (self.close_button) { self.close_button.remove(); self.close_button = null; } selected = false; } for (var i = 0; i < self.connectors.length; i++) { self.connectors[i].hide(); } }; var remove = function () { if (self.close_button) { self.close_button.remove(); } for (var i = 0; i < self.connectors.length; i++) { self.connectors[i].remove(); } graph.remove_node(self); node_fig.remove(); node_label.remove(); }; this.set_pos = set_pos; this.get_pos = get_pos; this.set_label = set_label; this.get_label = get_label; this.get_bound = get_bound; this.get_fig = get_fig; this.set_selected = set_selected; this.set_not_selected = set_not_selected; this.update_linked_edges = update_linked_edges; this.remove = remove; // Select the node and play an animation when clicked var click_action = function () { if (type === 'circle') { node_fig.attr({'rx': sx / 2 + 3, 'ry': sy / 2 + 3}); node_fig.animate({'rx': sx / 2, 'ry': sy / 2}, 500, 'elastic'); } else { var cx = get_pos().x; var cy = get_pos().y; node_fig.attr( { 'x': cx - sx / 2 - 3, 'y': cy - sy / 2 - 3, 'width': sx + 6, 'height': sy + 6, } ); node_fig.animate( { 'x': cx - sx / 2, 'y': cy - sy / 2, 'width': sx, 'height':sy, }, 500, 'elastic' ); } graph.select(self); }; node_fig.click(click_action); node_label.click(click_action); // Move the node when dragged var drag_down = function () { this.opos = get_pos(); }; var drag_move = function (dx, dy) { // We disable labels when moving for performance reasons, // updating the label position is quite expensive // we put this here because drag_down is also called on // simple clicks ... and this causes unwanted flicker // Disable drag move when graph is readonly if (graph.readonly) { return } var edges = graph.get_linked_edge_list(self); for (var i = 0; i < edges.length; i++) { edges[i].label_disable(); } if (self.close_button) { self.close_button.hide(); } set_pos(this.opos.add_xy(dx, dy)); this.start_move_graph_node = true; }; var drag_up = function () { // We re-enable the var edges = graph.get_linked_edge_list(self); for (var i = 0; i < edges.length; i++) { edges[i].label_enable(); } if (self.close_button) { self.close_button.show(); } if (this.start_move_graph_node) { this.start_move_graph_node = false; GraphNode.drag_up_callback(self); } }; node_fig.drag(drag_move, drag_down, drag_up); node_label.drag(drag_move, drag_down, drag_up); // Allow the user to create edges by dragging onto the node function hover_in () { if (graph.creating_edge) { graph.target_node = self; } } function hover_out () { graph.target_node = null; } node_fig.hover(hover_in, hover_out); node_label.hover(hover_in, hover_out); function double_click () { GraphNode.double_click_callback(self); } node_fig.dblclick(double_click); node_label.dblclick(double_click); this.connectors.push(new Connector(graph, this, - sx / 2, 0)); this.connectors.push(new Connector(graph, this, sx / 2, 0)); this.connectors.push(new Connector(graph, this, 0, - sy / 2)); this.connectors.push(new Connector(graph, this, 0, sy / 2)); // Disable close button creation when graph is readonly this.close_button = graph.readonly ? false :new CloseButton( graph, this, "node", sx / 2, - sy / 2); } GraphNode.double_click_callback = function (node) { // eslint-disable-next-line no-console console.log("double click from node:", node); }; // This is the default node destruction callback. // It is called before the node is removed from the graph // and before the connected edges are destroyed // eslint-disable-next-line no-unused-vars GraphNode.destruction_callback = function (node) { return true; }; GraphNode.drag_up_callback = function (node) { // eslint-disable-next-line no-console console.log("drag up node:", node); }; // Creates a new edge with label 'label' from start to end. // Start and end must implement get_pos_*, // if tmp is true, the edge is not added to the graph, used for drag edges. // replace tmp == false by graph == null function GraphEdge (graph, label, start, end, tmp) { var self = this; var r = graph.r; // The 0 = straight, != 0 curved var curvature = 0; // Positions of the start and end point of the // line between start and end // eslint-disable-next-line init-declarations var s, e; // Position of the middle of the curve (bezier control point) // eslint-disable-next-line init-declarations var mc; // Control points of the cubic bezier for the loop edges // eslint-disable-next-line init-declarations var mc1, mc2; var elfs = graph.style.edge_label_font_size || 10; var label_enabled = true; // Unique id used to order the curved edges this.uid = 0; // The svg definition of the edge vector path var edge_path = ""; var selected = false; if (!tmp) { graph.add_edge(start, end, this); } // Return the position of the label function get_label_pos (path) { var cpos = path.getTotalLength() * 0.5; var cindex = Math.abs(Math.floor(curvature)); var mod = cindex % 3 * elfs * 3.1 - elfs * 0.5; var verticality = Math.abs( end.get_pos().sub(start.get_pos()).normalize().dot_xy(0, 1)); verticality = Math.max(verticality - 0.5, 0) * 2; var lpos = path.getPointAtLength(cpos + mod * verticality); return new Vec2(lpos.x, lpos.y - elfs * (1 - verticality)); } // Used by close_button this.get_pos = function () { // eslint-disable-next-line no-use-before-define if (!edge) { return start.get_pos().lerp(end.get_pos(), 0.5); } // eslint-disable-next-line no-use-before-define return get_label_pos(edge); // The var bbox = edge_label.getBBox(); Does not work... :( // return new Vec2(bbox.x + bbox.width, bbox.y); }; // Straight line from s to e function make_line () { return "M" + s.x + "," + s.y + "L" + e.x + "," + e.y; } // Curved line from s to e by mc function make_curve () { return "M" + s.x + "," + s.y + "Q" + mc.x + "," + mc.y + " " + e.x + "," + e.y; } // Curved line from s to e by mc1 mc2 function make_loop () { return "M" + s.x + " " + s.y + "C" + mc1.x + " " + mc1.y + " " + mc2.x + " " + mc2.y + " " + e.x + " " + e.y; } // Computes new start and end line coordinates function update_curve () { // eslint-disable-next-line init-declarations var col; // eslint-disable-next-line no-negated-condition if (start !== end) { // eslint-disable-next-line no-negated-condition if (!tmp) { curvature = graph.get_edge_curvature(start, end, self); } else { curvature = 0; } s = start.get_pos(); e = end.get_pos(); // Middle of the line s->e mc = s.lerp(e, 0.5); var se = e.sub(s); se = se.normalize(); se = se.rotate_deg(- 90); se = se.scale(curvature * graph.style.edge_spacing); mc = mc.add(se); if (start.get_bound) { col = start.get_bound().collide_segment(s, mc); if (col.length > 0) { s = col[0]; } } if (end.get_bound) { col = end.get_bound().collide_segment(mc, e); if (col.length > 0) { e = col[0]; } } // eslint-disable-next-line no-negated-condition if (curvature !== 0) { edge_path = make_curve(); } else { edge_path = make_line(); } } else { // The start == end var rad = graph.style.edge_loop_radius || 100; s = start.get_pos(); e = end.get_pos(); var r_ = Vec2.new_polar_deg( rad, graph.get_loop_angle(start, self)); mc = s.add(r_); var p = r_.rotate_deg(90); mc1 = mc.add(p.set_len(rad * 0.5)); mc2 = mc.add(p.set_len(- rad * 0.5)); if (start.get_bound) { col = start.get_bound().collide_segment(s, mc1); if (col.length > 0) { s = col[0]; } col = start.get_bound().collide_segment(e, mc2); if (col.length > 0) { e = col[0]; } } edge_path = make_loop(); } } update_curve(); var edge = r.path(edge_path).attr( { 'stroke': graph.style.edge_color, 'stroke-width': graph.style.edge_width, 'arrow-end': 'block-wide-long', 'cursor':'pointer', } ).insertBefore(graph.get_node_list()[0].get_fig()); var labelpos = get_label_pos(edge); var edge_label = r.text(labelpos.x, labelpos.y - elfs, label).attr( { 'fill': graph.style.edge_label_color, 'cursor': 'pointer', 'font-size': elfs, } ); edge.transform(graph.get_transform()); graph.set_scrolling(edge); edge_label.transform(graph.get_transform()); graph.set_scrolling(edge_label); // Since we create an edge we need to recompute the edges that have the // same start and end positions as this one if (!tmp) { var edges_start = graph.get_linked_edge_list(start); var edges_end = graph.get_linked_edge_list(end); var edges = edges_start.length < edges_end.length ? edges_start : edges_end; for (var i = 0; i < edges.length; i ++) { if (edges[i] !== self) { edges[i].update(); } } } function label_enable () { if (!label_enabled) { label_enabled = true; edge_label.animate({'opacity': 1}, 100, 'linear'); if (self.close_button) { self.close_button.show(); } self.update(); } } function label_disable () { if (label_enabled) { label_enabled = false; edge_label.animate({'opacity': 0}, 100, 'linear'); if (self.close_button) { self.close_button.hide(); } } } // Update the positions function update () { update_curve(); edge.attr({'path':edge_path}); if (label_enabled) { var labelpos_ = get_label_pos(edge); edge_label.attr({'x': labelpos_.x, 'y': labelpos_.y - 14}); } } // Removes the edge from the scene, disconnects it from linked // nodes, destroy its drawable elements. function remove () { edge.remove(); edge_label.remove(); if (!tmp) { graph.remove_edge(self); } if (start.update_linked_edges) { start.update_linked_edges(); } if (start !== end && end.update_linked_edges) { end.update_linked_edges(); } if (self.close_button) { self.close_button.remove(); } } this.set_selected = function () { if (!selected) { selected = true; edge.attr( { 'stroke': graph.style.node_selected_color, 'stroke-width': graph.style.node_selected_width, } ); edge_label.attr({'fill': graph.style.node_selected_color}); if (!self.close_button) { self.close_button = new CloseButton( graph, self, "edge", 0, 30); self.close_button.show(); } } }; this.set_not_selected = function () { if (selected) { selected = false; edge.animate( { 'stroke': graph.style.edge_color, 'stroke-width': graph.style.edge_width, }, 100, 'linear' ); edge_label.animate( {'fill': graph.style.edge_label_color}, 100, 'linear' ); if (self.close_button) { self.close_button.remove(); self.close_button = null; } } }; function click_action () { graph.select(self); } edge.click(click_action); edge_label.click(click_action); function double_click_action () { GraphEdge.double_click_callback(self); } edge.dblclick(double_click_action); edge_label.dblclick(double_click_action); this.label_enable = label_enable; this.label_disable = label_disable; this.update = update; this.remove = remove; this.is_loop = function () { return start === end; }; this.get_start = function () { return start; }; this.get_end = function () { return end; }; } GraphEdge.double_click_callback = function (edge) { // eslint-disable-next-line no-console console.log("double click from edge:", edge); }; // This is the default edge creation callback. // It is called before an edge is created. // It returns an object containing the properties of the edge. // If it returns null, the edge is not created. // eslint-disable-next-line no-unused-vars GraphEdge.creation_callback = function (start, end) { var edge_prop = {}; edge_prop.label = 'new edge!'; return edge_prop; }; // This is is called after a new edge is created, with the new edge // as parameter // eslint-disable-next-line no-empty-function, no-unused-vars GraphEdge.new_edge_callback = function (new_edge) {}; // This is the default edge destruction callback. It is called before // an edge is removed from the graph. // eslint-disable-next-line no-unused-vars GraphEdge.destruction_callback = function (edge) { return true; }; // Returns a new string with the same content as str, // but with lines of maximum 'width' characters. // lines are broken on words, or // into words if a word is longer than 'width' function wordwrap (str, width) { // http://james.padolsey.com/javascript/wordwrap-for-javascript/ var width_norm = width || 32; var cut = true; var brk = '\n'; if (!str) { return str; } var regex = '.{1,' + width_norm + '}(\\s|$)' + (cut ? '|.{' + width_norm + '}|.+$' : '|\\S+?(\\s|$)'); return str.match(new RegExp(regex, 'g')).join(brk); } export { Graph, GraphNode, GraphEdge, wordwrap, };