|
|
|
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 node_text_layout = {
|
|
|
|
x: 1 - node_size,
|
|
|
|
y: 2 - node_size,
|
|
|
|
text: name,
|
|
|
|
fontSize: 22,
|
|
|
|
width: 2 * node_size - 1,
|
|
|
|
height: 2 * node_size - 1,
|
|
|
|
fontFamily: 'Roboto, Arial, Verdana, sans-serif',
|
|
|
|
fill: 'black',
|
|
|
|
verticalAlign: 'middle',
|
|
|
|
align: 'center'
|
|
|
|
};
|
|
|
|
|
|
|
|
const edge_layout = {
|
|
|
|
stroke: 'black',
|
|
|
|
strokeWidth: 3,
|
|
|
|
}
|
|
|
|
|
|
|
|
const edge_text_layout = {
|
|
|
|
fontSize: 22,
|
|
|
|
width: 2 * node_size,
|
|
|
|
height: 2 * node_size,
|
|
|
|
fontFamily: 'Roboto, Arial, Verdana, sans-serif',
|
|
|
|
fill: 'black',
|
|
|
|
verticalAlign: 'middle',
|
|
|
|
align: 'center'
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-------------------------------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";
|
|
|
|
|
|
|
|
// selected nodes
|
|
|
|
var selected = [];
|
|
|
|
var path_elements = [];
|
|
|
|
|
|
|
|
// (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_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) {
|
|
|
|
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, 141, 25)");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
};
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//---------------------------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") {
|
|
|
|
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) {
|
|
|
|
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";
|
|
|
|
})
|
|
|
|
.catch(error => console.error('Error:', error));
|
|
|
|
}, function() {state = "idle"});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function create_edge() {
|
|
|
|
if (selected.length < 2) {
|
|
|
|
show_error("Select at least 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) {
|
|
|
|
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(),
|
|
|
|
"weight": text
|
|
|
|
});
|
|
|
|
selected = [];
|
|
|
|
draw_selected_nodes();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.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(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();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function find_path(data) {
|
|
|
|
if (data.length < 2) {
|
|
|
|
show_error("Select at least 2 nodes.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
initialization();
|
|
|
|
|
|
|
|
// bug: this avoids glitches
|
|
|
|
window.onfocus = function() {focus = true;};
|
|
|
|
window.onblur = function() {focus = false;};
|