Files
gzth/crnd_web_diagram_plus/static/lib/js/graph.js
T
2024-08-10 16:41:35 +08:00

1228 lines
42 KiB
JavaScript

/** @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,
};