Frontend requiremets finished

delete_edge
Ugo Finnendahl 5 years ago
parent 8494cc6992
commit e2bb890704
  1. 8
      frontend/assets/css/src/style.scss
  2. 2
      frontend/assets/css/style.min.css
  3. 580
      frontend/assets/js/canvas.js
  4. 28
      frontend/index.html

@ -37,6 +37,14 @@ main{
position: relative;
height: 100%;
.background{
position: absolute;
top:0;
left:0;
background-color: rgba(0,0,0,0.5);
width: 100%;
height: 100%;
}
input{
position: absolute;
border: none;

@ -1 +1 @@
*{margin:0;padding:0;box-sizing:border-box}body{font-family:Roboto}main{position:relative;height:100vh}main #infobar{position:absolute;top:0;width:100%;padding:0.4em;background-color:rgba(232,9,9,0.4);border:4px solid rgba(232,9,9,0.8);text-align:center;font-weight:bold;font-size:1.8em;z-index:1;transform:translateY(-100%);transition:transform 1s}main #infobar.active{transform:translateY(0%)}main .canvas{background-color:lightgrey;position:relative;height:100%}main .canvas input{position:absolute;border:none;padding:0.6em 1em;width:50px;transform:translateX(-50%) translateY(-50%)}main nav,main footer{display:flex;position:absolute;top:1em;left:1em;opacity:0.9}main nav button,main footer button{display:block;background-color:#939292;border:none;border-radius:1em;padding:1em 1em;min-width:8em;margin:0.4em;color:black;border:solid 2px #131313;box-shadow:2px 2px 8px rgba(0,0,0,0.5)}main nav button:focus,main footer button:focus{outline:none}main nav button::-moz-focus-inner,main footer button::-moz-focus-inner{border:0}main nav button:hover,main footer button:hover{background-color:#acacac;border-color:#000;cursor:pointer}main nav button.active,main footer button.active{background-color:#f08d19}main nav button h1,main footer button h1{text-align:center;font-size:1.4em;font-weight:normal;white-space:nowrap}main nav button img,main footer button img{display:block;margin:0 auto 0.5em auto;width:2em}main footer{top:auto;left:auto;bottom:1em;right:1em}
*{margin:0;padding:0;box-sizing:border-box}body{font-family:Roboto}main{position:relative;height:100vh}main #infobar{position:absolute;top:0;width:100%;padding:0.4em;background-color:rgba(232,9,9,0.4);border:4px solid rgba(232,9,9,0.8);text-align:center;font-weight:bold;font-size:1.8em;z-index:1;transform:translateY(-100%);transition:transform 1s}main #infobar.active{transform:translateY(0%)}main .canvas{background-color:lightgrey;position:relative;height:100%}main .canvas .background{position:absolute;top:0;left:0;background-color:rgba(0,0,0,0.5);width:100%;height:100%}main .canvas input{position:absolute;border:none;padding:0.6em 1em;width:50px;transform:translateX(-50%) translateY(-50%)}main nav,main footer{display:flex;position:absolute;top:1em;left:1em;opacity:0.9}main nav button,main footer button{display:block;background-color:#939292;border:none;border-radius:1em;padding:1em 1em;min-width:8em;margin:0.4em;color:black;border:solid 2px #131313;box-shadow:2px 2px 8px rgba(0,0,0,0.5)}main nav button:focus,main footer button:focus{outline:none}main nav button::-moz-focus-inner,main footer button::-moz-focus-inner{border:0}main nav button:hover,main footer button:hover{background-color:#acacac;border-color:#000;cursor:pointer}main nav button.active,main footer button.active{background-color:#f08d19}main nav button h1,main footer button h1{text-align:center;font-size:1.4em;font-weight:normal;white-space:nowrap}main nav button img,main footer button img{display:block;margin:0 auto 0.5em auto;width:2em}main footer{top:auto;left:auto;bottom:1em;right:1em}

@ -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,267 +77,340 @@ var path_elements = [];
// (additions) auto layout
var auto_layout = false;
function initialisation(){
stage.add(edge_layer);
stage.add(node_layer);
function initialization() {
stage.add(edge_layer);
stage.add(node_layer);
// avoid flickering
edge_layer.hide();
// avoid flickering
edge_layer.hide();
// draw initial graph
for (id in nodes) {
// 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) {
}
for (id in edges) {
draw_new_edge(edges[id]);
}
// (additions) auto layout
}
if ( sessionStorage.getItem("auto_layout") !== null) {
// (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){
if (auto_layout) {
document.querySelector("[data-activeon=auto_layout]").classList.add("active");
}
}
// Precalculate a good graph presentation
for (var i = 0; i < 1000; i++) {
}
// Precalculate a good graph presentation
for (var i = 0; i < 1000; i++) {
force_directed_graph();
}
node_layer.draw();
}
edge_layer.show();
}
initialization();
edge_layer.show();
}
initialisation();
//-----------------------------------------------------------------------------
//------------------------------utils------------------------------------------
//-----------------------------------------------------------------------------
function sort(array) {
return array.sort(function(a, b) {
return a - b;
})
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) {
const pos = {
x: x,
y: y
};
let group = new Konva.Group({
...pos,
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('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();
let group = new Konva.Group({
x: x,
y: y,
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('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;
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();
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(){
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");
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");
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();
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){
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();
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)");
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();
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"){
function create_node() {
if (state != "create_node") {
state = "create_node";
// let mousePos = stage.getPointerPosition();
let pos = {x: width/2, y: height/2};
create_input(pos.x, pos.y).then(function(text) {
draw_new_node(text, pos.x, pos.y);
state = "idle";
}, function(){state = "idle"});
}
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";
})
.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) {
draw_new_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();
},function(){});
});
selected = [];
draw_selected_nodes();
}
})
.catch(error => console.error('Error:', error));
}, function() {});
}
function create_input(x, y) {
return new Promise(function(resolve, reject) {
var input = document.createElement("input");
input.setAttribute("type", "text");
input.style.left = x + "px";
input.style.top = y + "px";
canvas.appendChild(input);
input.focus();
stage.listening(false);
window.addEventListener("keyup", function(event) {
if (event.key === "Escape") {
input.remove();
stage.listening(true);
reject();
}
});
input.addEventListener("keyup", function(event) {
if (event.key === "Enter") {
resolve(input.value);
input.remove();
stage.listening(true);
}
});
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(){
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]);
}
for (id in edges) {
update_edge(edges[id]);
}
}
node_layer.on('beforeDraw', function() {
update_edges(konva_edges);
update_edges(konva_edges);
});
// adapt the stage on any window resize
@ -346,91 +423,80 @@ 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");
sessionStorage.setItem("auto_layout", 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);
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){
let idx = graph_copy[start_node.attrs.id].indexOf(end_node.attrs.id);
if (idx < 0){
return false;
}
return true;
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 = ((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);
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 = ((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);
}
}
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;};

@ -20,7 +20,7 @@
<img src="{{ url_for('static', filename='imgs/drawing.svg') }}">
<h1>Connect</h1>
</button>
<button type="button" onclick="find_path()">
<button type="button" onclick="find_path(selected)">
<img src="{{ url_for('static', filename='imgs/share.svg') }}">
<h1>Find Path</h1>
</button>
@ -32,29 +32,9 @@
</footer>
</main>
<script>
var nodes = ["Aachen", "Berlin", "Chemnitz", "Dresden", "Essen"]
var edges = [{
"start": "Aachen",
"end": "Berlin",
"weight": 800
},{
"start": "Dresden",
"end": "Berlin",
"weight": 800
},{
"start": "Essen",
"end": "Berlin",
"weight": 800
}, {
"start": "Chemnitz",
"end": "Dresden",
"weight": 5
},
{
"start": "Chemnitz",
"end": "Berlin",
"weight": 5
}]
var nodes = {{ nodes|safe }};
var edges = {{ edges|safe }};
const url = "http://{{ request.host|safe }}/api/";
</script>
<script src="{{ url_for('static', filename='js/canvas.js')}}"></script>
</body>

Loading…
Cancel
Save