const canvas = document.getElementById("canvas"); //----------------------------------------------------------------------------- //-----------------------------Layouts----------------------------------------- //----------------------------------------------------------------------------- var width = canvas.offsetWidth; var height = canvas.offsetHeight; const node_size = 55; var stage = new Konva.Stage({ container: canvas, width: width, height: height }); const node_layout = { radius: node_size, fill: 'white', stroke: 'black', strokeWidth: 2, shadowColor: 'black', shadowBlur: 10, shadowOffset: { x: 2, y: 2 }, shadowOpacity: 0.3, }; const text_layout = { fontSize: 22, fontFamily: 'Roboto, Arial, sans-serif', fill: 'black', verticalAlign: 'middle', align: 'center' }; const node_text_layout = { ...text_layout, x: 1 - node_size, y: 2 - node_size, width: 2 * node_size - 1, height: 2 * node_size - 1, }; const edge_layout = { stroke: 'black', strokeWidth: 3, }; const edge_text_layout = { ...text_layout, width: 2 * node_size, height: 2 * node_size, }; //----------------------------------------------------------------------------- //-------------------------------Init------------------------------------------ //----------------------------------------------------------------------------- var edge_layer = new Konva.Layer(); var node_layer = new Konva.Layer(); var konva_edges = []; var konva_nodes = []; var graph_copy = {}; var state = "idle"; var info_timer = {}; // selected nodes var selected = []; // (additions) auto layout var auto_layout = false; function initialization() { stage.add(edge_layer); stage.add(node_layer); // avoid flickering edge_layer.hide(); // draw initial graph for (id in nodes) { draw_new_node(nodes[id], node_size + Math.random() * (width - 2 * node_size), node_size + Math.random() * (height - 2 * node_size)); } for (id in edges) { draw_new_edge(edges[id]); } // (additions) auto layout if (sessionStorage.getItem("auto_layout") !== null) { // Restore the contents of the text field auto_layout = (sessionStorage.getItem("auto_layout") == 'true'); if (auto_layout) { document.querySelector("[data-activeon=auto_layout]").classList.add("active"); } } // Precalculate a good graph presentation for (var i = 0; i < 1000; i++) { force_directed_graph(); } edge_layer.show(); node_layer.draw(); } //----------------------------------------------------------------------------- //------------------------------utils------------------------------------------ //----------------------------------------------------------------------------- function sort(array) { return array.sort(function(a, b) { return a - b; }) } function show_info(msg, elem="#errorbar") { let infobar = document.querySelector(elem); infobar.innerHTML = msg; infobar.classList.add("active"); if (info_timer[elem]){ clearTimeout(info_timer[elem]); } info_timer[elem] = window.setTimeout(function() { infobar.classList.remove("active"); }, 4000); } //----------------------------------------------------------------------------- //---------------------------draw methods-------------------------------------- //----------------------------------------------------------------------------- function draw_new_node(name, x, y) { let group = new Konva.Group({ x: x, y: y, dragDistance: 5, draggable: true, dragBoundFunc: function(pos) { return { x: sort([node_size, pos.x, width - node_size])[1], y: sort([node_size, pos.y, height - node_size])[1], }; }, id: name }); let box = new Konva.Circle(node_layout); let simpleText = new Konva.Text({ ...node_text_layout, text: name }); group.add(box); group.add(simpleText); group.on("click", on_node_click); group.on('tap', on_node_click); group.on('dragstart', function() { group.attrs.dragging = true; }); group.on('dragend', function() { group.attrs.dragging = false; }); group.on('mouseover', function() { document.body.style.cursor = 'pointer'; }); group.on('mouseout', function() { document.body.style.cursor = 'default'; }); graph_copy[name] = []; konva_nodes.push(group); node_layer.add(group); node_layer.draw(); } function draw_new_edge(edge) { let start_node = stage.findOne('#' + edge["start"]); let end_node = stage.findOne('#' + edge["end"]); let weight = edge["weight"]; let group = new Konva.Group({ start_node: start_node, end_node: end_node, weight: weight, id: edge["start"] + edge["end"] }); let line = new Konva.Line(edge_layout); let simpleText = new Konva.Text({ ...edge_text_layout, text: ""+weight }); graph_copy[edge["start"]].push(edge["end"]); graph_copy[edge["end"]].push(edge["start"]); group.add(line); group.add(simpleText); konva_edges.push(group); update_edge(group); edge_layer.add(group); edge_layer.draw(); return group; } function update_edge(konva_edge) { let line = konva_edge.children[0]; let text = konva_edge.children[1]; let start_node = konva_edge.attrs.start_node; let end_node = konva_edge.attrs.end_node; let size = 15 + text.text().length * 15; let line_length = Math.sqrt((start_node.getX() - end_node.getX()) ** 2 + (start_node.getY() - end_node.getY()) ** 2) - size; line.points([start_node.getX(), start_node.getY(), end_node.getX(), end_node.getY()]); line.dash([line_length / 2, size, line_length / 2]); text.x((start_node.getX() + end_node.getX()) / 2 - node_size); text.y((start_node.getY() + end_node.getY()) / 2 - node_size), edge_layer.draw(); } function reset_node_style() { for (let id in konva_nodes) { konva_nodes[id].children[0].strokeWidth(node_layout.strokeWidth); konva_nodes[id].children[0].stroke("black"); } } function reset_edge_style() { for (let id in konva_edges) { konva_edges[id].children[0].strokeWidth(edge_layout.strokeWidth); konva_edges[id].children[0].stroke("black"); } } function draw_selected_nodes() { reset_node_style(); reset_edge_style(); for (let id in selected) { selected[id].children[0].strokeWidth(node_layout.strokeWidth+1); selected[id].children[0].stroke("rgb(240, 141, 25)"); } node_layer.draw(); } function draw_path(path) { let path_elements = []; for (let id in path) { path_elements.push(stage.findOne("#" + path[id])); if (id == 0) { continue } let current_edge = stage.findOne("#" + path[id - 1] + path[id]); if (current_edge) { path_elements.push(current_edge); } else { path_elements.push(stage.findOne("#" + path[id] + path[id - 1])); } } reset_node_style(); reset_edge_style(); for (let id in path_elements) { path_elements[id].children[0].strokeWidth(node_layout.strokeWidth + 1); path_elements[id].children[0].stroke("rgb(240, 44, 25)"); } node_layer.draw(); edge_layer.draw(); } //----------------------------------------------------------------------------- function on_node_click(e) { e.cancelBubble = true; // let mousePos = stage.getPointerPosition(); let idx = selected.indexOf(this); if (idx >= 0) { selected.splice(idx, 1); } else if (selected.length < 2) { selected.push(this); } else { selected.shift(); selected.push(this); } draw_selected_nodes(); }; function create_input(x, y) { return new Promise(function(resolve, reject) { var container = document.createElement("div"); container.classList.add("background"); var input = document.createElement("input"); var clear = function() { container.remove(); stage.listening(true); focus = true; } input.setAttribute("type", "text"); input.style.left = x + "px"; input.style.top = y + "px"; canvas.appendChild(container); container.appendChild(input); input.focus(); stage.listening(false); focus = false; window.addEventListener("keyup", function(event) { if (event.key === "Escape") { reject(); clear(); } }); input.addEventListener("keyup", function(event) { if (event.key === "Enter") { resolve(input.value); clear(); } }); }); } //----------------------------------------------------------------------------- //---------------------------REST methods-------------------------------------- //----------------------------------------------------------------------------- function rest_req(method, endpoint, data) { let body_data = {} if(!!data){ body_data = {body: JSON.stringify(data)}; } return fetch(url + "api/" + endpoint, { method: method, ...body_data, headers: { 'Content-Type': 'application/json' } }).then(res => res.json()) } //----------------------------------------------------------------------------- function create_node() { if (state != "create_node") { state = "create_node"; // let mousePos = stage.getPointerPosition(); let pos = { x: (Math.random()-0.5)*node_size*4+width / 2, y: (Math.random()-0.5)*node_size*4+height / 2 }; create_input(pos.x, pos.y).then(function(text) { rest_req('POST', "nodes", { name: text }) .then(function(res) { if (res["error"]) { show_info(res["error"]) } else { draw_new_node(text, pos.x, pos.y); } state = "idle"; }) .catch(error => console.error('Error:', error)); }, function() {state = "idle"}); } } function create_edge() { if (selected.length < 2) { show_info("Select 2 nodes."); return; } let x = (selected[0].getX() + selected[1].getX()) / 2; let y = (selected[0].getY() + selected[1].getY()) / 2; create_input(x, y).then(function(text) { rest_req('POST',"edges", { start: selected[0].id(), end: selected[1].id(), weight: text }) .then(function(res) { if (res["error"]) { show_info(res["error"]) } else { draw_new_edge(res); selected = []; draw_selected_nodes(); } }) .catch(error => console.error('Error:', error)); }, function() {}); } function find_path(data) { if (data.length < 2) { show_info("Select 2 nodes."); return; } rest_req("GET", "paths/" + data[0].attrs.id + "/" + data[1].attrs.id, null) .then(function(res) { if (res["error"]) { show_info(res["error"]); } else { selected = []; draw_path(res["path"]); show_info(res["path"], "#infobar"); } }) .catch(error => console.error('Error:', error)); } //----------------------------------------------------------------------------- function save_graph() { fetch(url+"save").then(res => res.json()) .then(function(res) { if (res["error"]) { show_info(res["error"]); } else { show_info(res["info"], "#infobar"); } }) .catch(error => console.error('Error:', error)); } function clear_graph() { fetch(url+"clear").then(res => res.json()) .then(function(res) { if (res["error"]) { show_info(res["error"]); } else { window.location.reload(false); } }) .catch(error => console.error('Error:', error)); } //----------------------------------------------------------------------------- //------------------------background methods----------------------------------- //----------------------------------------------------------------------------- function update_edges(edges) { for (id in edges) { update_edge(edges[id]); } } node_layer.on('beforeDraw', function() { update_edges(konva_edges); }); // adapt the stage on any window resize function fitStageIntoParentContainer() { width = canvas.offsetWidth; height = canvas.offsetHeight; stage.width(canvas.offsetWidth); stage.height(canvas.offsetHeight); stage.draw(); } window.addEventListener('resize', fitStageIntoParentContainer); //---------------------(additions) auto layout--------------------------------- function toggle_auto_layout() { auto_layout = !auto_layout; document.querySelector("[data-activeon=auto_layout]").classList.toggle("active"); sessionStorage.setItem("auto_layout", auto_layout); } function normalize(vec) { let l = Math.sqrt(vec[0] ** 2 + vec[1] ** 2); if (l == 0){ return [vec[0], vec[1]]; } return [vec[0] / l, vec[1] / l]; } function in_edges(start_node, end_node) { let idx = graph_copy[start_node.attrs.id].indexOf(end_node.attrs.id); if (idx < 0) { return false; } return true; } function force_directed_graph(spring_length = 300, step = 0.004) { for (var idx in konva_nodes) { if (konva_nodes[idx].attrs.dragging) { continue } var x1 = konva_nodes[idx].getX(); var y1 = konva_nodes[idx].getY(); var rep = [0, 0]; var attr = [0, 0]; for (let jdx in konva_nodes) { if (idx == jdx) { continue; } let x2 = konva_nodes[jdx].getX(); let y2 = konva_nodes[jdx].getY(); let dir = normalize([x2 - x1, y2 - y1]); let dist = Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2); if (dist == 0){ continue; } if (in_edges(konva_nodes[idx], konva_nodes[jdx])) { let f = (dist**2 / spring_length); attr[0] += f * dir[0]; attr[1] += f * dir[1]; } if (dist < width*0.4){ let f = (spring_length ** 2 / dist); rep[0] -= f * dir[0]; rep[1] -= f * dir[1]; } } let new_x = x1 + step * (rep[0] + attr[0]); let new_y = y1 + step * (rep[1] + attr[1]); if (new_x < 2 * node_size) { new_x += 10 * step * ((2 * node_size - new_x)); } else if (new_x > width - 2 * node_size) { new_x += 10 * step * ((width - 2 * node_size - new_x)); } if (new_y < 2 * node_size) { new_y += 10 * step * ((2 * node_size - new_y)); } else if (new_y > height - 2 * node_size) { new_y += 10 * step * ((height - 2 * node_size - new_y)); } konva_nodes[idx].x(new_x); konva_nodes[idx].y(new_y); } } var anim = new Konva.Animation(function(frame) { if (auto_layout && focus) { const step = 0.00025 * frame.timeDiff; let spring_factor = Math.min((konva_edges.length+konva_nodes.length-3)/20, 1); force_directed_graph(150+(1-spring_factor)*180, step); } }, node_layer); anim.start(); // bug: this avoids glitches window.onfocus = function() {focus = true;}; window.onblur = function() {focus = false;}; initialization();