|
|
|
@ -1,6 +1,8 @@ |
|
|
|
|
const canvas = document.getElementById("canvas"); |
|
|
|
|
|
|
|
|
|
// Layouts
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
//-----------------------------Layouts-----------------------------------------
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
var width = canvas.offsetWidth; |
|
|
|
|
var height = canvas.offsetHeight; |
|
|
|
|
|
|
|
|
@ -31,8 +33,8 @@ const node_text_layout = { |
|
|
|
|
y: 2 - node_size, |
|
|
|
|
text: name, |
|
|
|
|
fontSize: 22, |
|
|
|
|
width: 2 * node_size-1, |
|
|
|
|
height: 2 * node_size-1, |
|
|
|
|
width: 2 * node_size - 1, |
|
|
|
|
height: 2 * node_size - 1, |
|
|
|
|
fontFamily: 'Roboto', |
|
|
|
|
fill: 'black', |
|
|
|
|
verticalAlign: 'middle', |
|
|
|
@ -54,6 +56,7 @@ const edge_text_layout = { |
|
|
|
|
align: 'center' |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
//-------------------------------Init------------------------------------------
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
@ -62,6 +65,7 @@ var node_layer = new Konva.Layer(); |
|
|
|
|
|
|
|
|
|
var konva_edges = []; |
|
|
|
|
var konva_nodes = []; |
|
|
|
|
|
|
|
|
|
var graph_copy = {}; |
|
|
|
|
|
|
|
|
|
var state = "idle"; |
|
|
|
@ -73,10 +77,9 @@ var path_elements = []; |
|
|
|
|
// (additions) auto layout
|
|
|
|
|
var auto_layout = false; |
|
|
|
|
|
|
|
|
|
function initialisation(){ |
|
|
|
|
function initialization() { |
|
|
|
|
stage.add(edge_layer); |
|
|
|
|
stage.add(node_layer); |
|
|
|
|
|
|
|
|
|
// avoid flickering
|
|
|
|
|
edge_layer.hide(); |
|
|
|
|
|
|
|
|
@ -89,11 +92,10 @@ function initialisation(){ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// (additions) auto layout
|
|
|
|
|
|
|
|
|
|
if ( sessionStorage.getItem("auto_layout") !== null) { |
|
|
|
|
if (sessionStorage.getItem("auto_layout") !== null) { |
|
|
|
|
// Restore the contents of the text field
|
|
|
|
|
auto_layout = (sessionStorage.getItem("auto_layout") == 'true'); |
|
|
|
|
if (auto_layout){ |
|
|
|
|
if (auto_layout) { |
|
|
|
|
document.querySelector("[data-activeon=auto_layout]").classList.add("active"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -101,13 +103,12 @@ function initialisation(){ |
|
|
|
|
for (var i = 0; i < 1000; i++) { |
|
|
|
|
force_directed_graph(); |
|
|
|
|
} |
|
|
|
|
node_layer.draw(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
edge_layer.show(); |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
initialisation(); |
|
|
|
|
initialization(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
//------------------------------utils------------------------------------------
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
@ -116,16 +117,24 @@ function sort(array) { |
|
|
|
|
return a - b; |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function show_error(msg) { |
|
|
|
|
let infobar = document.querySelector("#infobar"); |
|
|
|
|
infobar.innerHTML = msg; |
|
|
|
|
infobar.classList.add("active"); |
|
|
|
|
window.setTimeout(function() { |
|
|
|
|
infobar.classList.remove("active"); |
|
|
|
|
}, 4000); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
//---------------------------draw methods--------------------------------------
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
function draw_new_node(name, x, y) { |
|
|
|
|
const pos = { |
|
|
|
|
x: x, |
|
|
|
|
y: y |
|
|
|
|
}; |
|
|
|
|
let group = new Konva.Group({ |
|
|
|
|
...pos, |
|
|
|
|
x: x, |
|
|
|
|
y: y, |
|
|
|
|
draggable: true, |
|
|
|
|
dragBoundFunc: function(pos) { |
|
|
|
|
return { |
|
|
|
@ -136,7 +145,10 @@ function draw_new_node(name, x, y) { |
|
|
|
|
id: name |
|
|
|
|
}); |
|
|
|
|
let box = new Konva.Circle(node_layout); |
|
|
|
|
let simpleText = new Konva.Text({...node_text_layout, text: name}); |
|
|
|
|
let simpleText = new Konva.Text({ |
|
|
|
|
...node_text_layout, |
|
|
|
|
text: name |
|
|
|
|
}); |
|
|
|
|
group.add(box); |
|
|
|
|
group.add(simpleText); |
|
|
|
|
group.on("click", on_node_click); |
|
|
|
@ -168,10 +180,13 @@ function draw_new_edge(edge) { |
|
|
|
|
start_node: start_node, |
|
|
|
|
end_node: end_node, |
|
|
|
|
weight: weight, |
|
|
|
|
id: edge["start"]+edge["end"] |
|
|
|
|
id: edge["start"] + edge["end"] |
|
|
|
|
}); |
|
|
|
|
let line = new Konva.Line(edge_layout); |
|
|
|
|
let simpleText = new Konva.Text({...edge_text_layout, text: weight}); |
|
|
|
|
let simpleText = new Konva.Text({ |
|
|
|
|
...edge_text_layout, |
|
|
|
|
text: weight |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
graph_copy[edge["start"]].push(edge["end"]); |
|
|
|
|
graph_copy[edge["end"]].push(edge["start"]); |
|
|
|
@ -192,8 +207,9 @@ function update_edge(konva_edge) { |
|
|
|
|
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 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); |
|
|
|
@ -202,14 +218,14 @@ function update_edge(konva_edge) { |
|
|
|
|
edge_layer.draw(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function reset_node_style(){ |
|
|
|
|
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(){ |
|
|
|
|
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"); |
|
|
|
@ -226,18 +242,18 @@ function draw_selected_nodes() { |
|
|
|
|
node_layer.draw(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function draw_path(path){ |
|
|
|
|
function draw_path(path) { |
|
|
|
|
let path_elements = []; |
|
|
|
|
for (let id in path){ |
|
|
|
|
path_elements.push(stage.findOne("#"+path[id])); |
|
|
|
|
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){ |
|
|
|
|
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])); |
|
|
|
|
} else { |
|
|
|
|
path_elements.push(stage.findOne("#" + path[id] + path[id - 1])); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
reset_node_style(); |
|
|
|
@ -263,21 +279,44 @@ function on_node_click(e) { |
|
|
|
|
draw_selected_nodes(); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
//---------------------------REST methods--------------------------------------
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
function post(endpoint, data) { |
|
|
|
|
return fetch(url + endpoint, { |
|
|
|
|
method: 'POST', |
|
|
|
|
body: JSON.stringify(data), |
|
|
|
|
headers: { |
|
|
|
|
'Content-Type': 'application/json' |
|
|
|
|
} |
|
|
|
|
}).then(res => res.json()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function create_node(){ |
|
|
|
|
if (state != "create_node"){ |
|
|
|
|
function create_node() { |
|
|
|
|
if (state != "create_node") { |
|
|
|
|
state = "create_node"; |
|
|
|
|
// let mousePos = stage.getPointerPosition();
|
|
|
|
|
let pos = {x: width/2, y: height/2}; |
|
|
|
|
let pos = { |
|
|
|
|
x: width / 2, |
|
|
|
|
y: height / 2 |
|
|
|
|
}; |
|
|
|
|
create_input(pos.x, pos.y).then(function(text) { |
|
|
|
|
post("nodes", { |
|
|
|
|
name: text |
|
|
|
|
}) |
|
|
|
|
.then(function(res) { |
|
|
|
|
if (res["error"]) { |
|
|
|
|
show_error(res["error"]) |
|
|
|
|
} else { |
|
|
|
|
draw_new_node(text, pos.x, pos.y); |
|
|
|
|
} |
|
|
|
|
state = "idle"; |
|
|
|
|
}, function(){state = "idle"}); |
|
|
|
|
}) |
|
|
|
|
.catch(error => console.error('Error:', error)); |
|
|
|
|
}, function() {state = "idle"}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function create_edge() { |
|
|
|
|
if (selected.length < 2) { |
|
|
|
|
show_error("Select at least 2 nodes."); |
|
|
|
@ -286,6 +325,15 @@ function create_edge() { |
|
|
|
|
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) { |
|
|
|
|
post("edges", { |
|
|
|
|
start: selected[0].id(), |
|
|
|
|
end: selected[1].id(), |
|
|
|
|
weight: text |
|
|
|
|
}) |
|
|
|
|
.then(function(res) { |
|
|
|
|
if (res["error"]) { |
|
|
|
|
show_error(res["error"]) |
|
|
|
|
} else { |
|
|
|
|
draw_new_edge({ |
|
|
|
|
"start": selected[0].children[1].text(), |
|
|
|
|
"end": selected[1].children[1].text(), |
|
|
|
@ -293,45 +341,74 @@ function create_edge() { |
|
|
|
|
}); |
|
|
|
|
selected = []; |
|
|
|
|
draw_selected_nodes(); |
|
|
|
|
},function(){}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
.catch(error => console.error('Error:', error)); |
|
|
|
|
|
|
|
|
|
}, function() {}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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(input); |
|
|
|
|
canvas.appendChild(container); |
|
|
|
|
container.appendChild(input); |
|
|
|
|
input.focus(); |
|
|
|
|
stage.listening(false); |
|
|
|
|
focus = false; |
|
|
|
|
window.addEventListener("keyup", function(event) { |
|
|
|
|
if (event.key === "Escape") { |
|
|
|
|
input.remove(); |
|
|
|
|
stage.listening(true); |
|
|
|
|
reject(); |
|
|
|
|
clear(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
input.addEventListener("keyup", function(event) { |
|
|
|
|
if (event.key === "Enter") { |
|
|
|
|
resolve(input.value); |
|
|
|
|
input.remove(); |
|
|
|
|
stage.listening(true); |
|
|
|
|
clear(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function find_path(){ |
|
|
|
|
draw_path(elem); |
|
|
|
|
function find_path(data) { |
|
|
|
|
if (data.length < 2) { |
|
|
|
|
show_error("Select at least 2 nodes."); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
console.log(url + "paths/" + data[0].attrs.id + "/" + data[1].attrs.id); |
|
|
|
|
fetch(url + "paths/" + data[0].attrs.id + "/" + data[1].attrs.id).then(res => res.json()) |
|
|
|
|
.then(function(res) { |
|
|
|
|
if (res["error"]) { |
|
|
|
|
show_error(res["error"]); |
|
|
|
|
} else { |
|
|
|
|
selected = []; |
|
|
|
|
draw_path(res["path"]); |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
.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); |
|
|
|
|
}); |
|
|
|
@ -346,10 +423,8 @@ function fitStageIntoParentContainer() { |
|
|
|
|
} |
|
|
|
|
window.addEventListener('resize', fitStageIntoParentContainer); |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
//---------------------(additions) auto layout---------------------------------
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
//---------------------(additions) auto layout---------------------------------
|
|
|
|
|
function toggle_auto_layout() { |
|
|
|
|
auto_layout = !auto_layout; |
|
|
|
|
document.querySelector("[data-activeon=auto_layout]").classList.toggle("active"); |
|
|
|
@ -357,57 +432,57 @@ function toggle_auto_layout() { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function normalize(vec) { |
|
|
|
|
let l = Math.sqrt(vec[0]**2+vec[1]**2); |
|
|
|
|
return [vec[0]/l,vec[1]/l]; |
|
|
|
|
let l = Math.sqrt(vec[0] ** 2 + vec[1] ** 2); |
|
|
|
|
return [vec[0] / l, vec[1] / l]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function in_edges(start_node, end_node){ |
|
|
|
|
function in_edges(start_node, end_node) { |
|
|
|
|
let idx = graph_copy[start_node.attrs.id].indexOf(end_node.attrs.id); |
|
|
|
|
if (idx < 0){ |
|
|
|
|
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){ |
|
|
|
|
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){ |
|
|
|
|
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 = ((x1-x2)**2+(y1-y2)**2); |
|
|
|
|
if (in_edges(konva_nodes[idx], konva_nodes[jdx])){ |
|
|
|
|
let f = (dist/spring_length); |
|
|
|
|
attr[0] += f*dir[0]; |
|
|
|
|
attr[1] += f*dir[1]; |
|
|
|
|
} |
|
|
|
|
let f = (spring_length**2/Math.sqrt(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)); |
|
|
|
|
let dir = normalize([x2 - x1, y2 - y1]); |
|
|
|
|
let dist = ((x1 - x2) ** 2 + (y1 - y2) ** 2); |
|
|
|
|
if (in_edges(konva_nodes[idx], konva_nodes[jdx])) { |
|
|
|
|
let f = (dist / spring_length); |
|
|
|
|
attr[0] += f * dir[0]; |
|
|
|
|
attr[1] += f * dir[1]; |
|
|
|
|
} |
|
|
|
|
let f = (spring_length ** 2 / Math.sqrt(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); |
|
|
|
@ -415,22 +490,13 @@ function force_directed_graph(spring_length = 300, step = 0.004){ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var anim = new Konva.Animation(function(frame) { |
|
|
|
|
if (auto_layout) { |
|
|
|
|
const spring_length = 300; |
|
|
|
|
const step = 0.00025*frame.timeDiff; |
|
|
|
|
force_directed_graph(spring_length, step) |
|
|
|
|
if (auto_layout && focus) { |
|
|
|
|
const step = 0.00025 * frame.timeDiff; |
|
|
|
|
force_directed_graph(300, step) |
|
|
|
|
} |
|
|
|
|
}, node_layer); |
|
|
|
|
anim.start(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function show_error(msg){ |
|
|
|
|
let infobar = document.querySelector("#infobar"); |
|
|
|
|
infobar.innerHTML = msg; |
|
|
|
|
infobar.classList.add("active"); |
|
|
|
|
window.setTimeout(function(){ infobar.classList.remove("active"); }, 4000); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// bug: this avoids glitches
|
|
|
|
|
window.onfocus = function () {auto_layout = true;}; |
|
|
|
|
window.onblur = function () {auto_layout = false;}; |
|
|
|
|
window.onfocus = function() {focus = true;}; |
|
|
|
|
window.onblur = function() {focus = false;}; |
|
|
|
|