diff --git a/frontend/assets/css/src/style.scss b/frontend/assets/css/src/style.scss
new file mode 100644
index 0000000..91e31a1
--- /dev/null
+++ b/frontend/assets/css/src/style.scss
@@ -0,0 +1,102 @@
+$button-color: rgb(147, 146, 146);
+$main-color: rgb(240, 141, 25);
+
+*{
+ margin:0;
+ padding: 0;
+ box-sizing: border-box;
+}
+body{
+ font-family: Roboto;
+
+}
+main{
+ position: relative;
+ height: 100vh;
+
+ #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;
+ &.active{
+ transform: translateY(0%);
+ }
+ }
+
+ .canvas{
+ background-color: lightgrey;
+ position: relative;
+ height: 100%;
+
+ input{
+ position: absolute;
+ border: none;
+ padding: 0.6em 1em;
+ width: 50px;
+ transform: translateX(-50%) translateY(-50%);
+ }
+ }
+
+ nav, footer{
+ display: flex;
+ position: absolute;
+ top: 1em;
+ left: 1em;
+ opacity: 0.9;
+
+ button{
+ display: block;
+ background-color: $button-color;
+ border: none;
+ border-radius: 1em;
+ padding: 1em 1em;
+ min-width: 8em;
+ margin: 0.4em;
+ color: black;
+ border: solid 2px darken($button-color, 50);
+ box-shadow: 2px 2px 8px rgba(0,0,0,0.5);
+
+ &:focus {
+ outline: none;
+ }
+ &::-moz-focus-inner {
+ border: 0;
+ }
+ &:hover{
+ background-color: lighten($button-color, 10);
+ border-color: darken($button-color, 100);
+ cursor: pointer;
+ }
+ &.active{
+ background-color: $main-color;
+ }
+ h1{
+ text-align: center;
+ font-size: 1.4em;
+ font-weight: normal;
+
+ white-space: nowrap
+ }
+ img{
+ display: block;
+ margin: 0 auto 0.5em auto;
+ width: 2em;
+ }
+ }
+ }
+ footer{
+ top: auto;
+ left: auto;
+ bottom: 1em;
+ right: 1em;
+ }
+}
diff --git a/frontend/assets/css/style.min.css b/frontend/assets/css/style.min.css
new file mode 100644
index 0000000..aee426d
--- /dev/null
+++ b/frontend/assets/css/style.min.css
@@ -0,0 +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}
diff --git a/frontend/assets/fonts/Roboto-Regular.eot b/frontend/assets/fonts/Roboto-Regular.eot
new file mode 100644
index 0000000..b06f366
Binary files /dev/null and b/frontend/assets/fonts/Roboto-Regular.eot differ
diff --git a/frontend/assets/fonts/Roboto-Regular.svg b/frontend/assets/fonts/Roboto-Regular.svg
new file mode 100644
index 0000000..1c8971f
--- /dev/null
+++ b/frontend/assets/fonts/Roboto-Regular.svg
@@ -0,0 +1,11080 @@
+
+
+
diff --git a/frontend/assets/fonts/Roboto-Regular.ttf b/frontend/assets/fonts/Roboto-Regular.ttf
new file mode 100644
index 0000000..586d062
Binary files /dev/null and b/frontend/assets/fonts/Roboto-Regular.ttf differ
diff --git a/frontend/assets/fonts/Roboto-Regular.woff b/frontend/assets/fonts/Roboto-Regular.woff
new file mode 100644
index 0000000..bb7f7b4
Binary files /dev/null and b/frontend/assets/fonts/Roboto-Regular.woff differ
diff --git a/frontend/assets/fonts/Roboto-Regular.woff2 b/frontend/assets/fonts/Roboto-Regular.woff2
new file mode 100644
index 0000000..c817ca4
Binary files /dev/null and b/frontend/assets/fonts/Roboto-Regular.woff2 differ
diff --git a/frontend/assets/fonts/roboto.css b/frontend/assets/fonts/roboto.css
new file mode 100644
index 0000000..a8d4398
--- /dev/null
+++ b/frontend/assets/fonts/roboto.css
@@ -0,0 +1,12 @@
+@font-face {
+ font-family: 'Roboto-Regular';
+ src: url('Roboto-Regular.eot');
+ src: url('Roboto-Regular.eot?#iefix') format('embedded-opentype'),
+ url('Roboto-Regular.svg#Roboto-Regular') format('svg'),
+ url('Roboto-Regular.ttf') format('truetype'),
+ url('Roboto-Regular.woff') format('woff'),
+ url('Roboto-Regular.woff2') format('woff2');
+ font-weight: normal;
+ font-style: normal;
+}
+
diff --git a/frontend/assets/imgs/drawing.svg b/frontend/assets/imgs/drawing.svg
new file mode 100644
index 0000000..2788490
--- /dev/null
+++ b/frontend/assets/imgs/drawing.svg
@@ -0,0 +1,133 @@
+
+
+
+
diff --git a/frontend/assets/imgs/link.svg b/frontend/assets/imgs/link.svg
new file mode 100644
index 0000000..0eecc82
--- /dev/null
+++ b/frontend/assets/imgs/link.svg
@@ -0,0 +1,47 @@
+
+
+
diff --git a/frontend/assets/imgs/plus.svg b/frontend/assets/imgs/plus.svg
new file mode 100644
index 0000000..abc77de
--- /dev/null
+++ b/frontend/assets/imgs/plus.svg
@@ -0,0 +1,44 @@
+
+
+
diff --git a/frontend/assets/imgs/share.svg b/frontend/assets/imgs/share.svg
new file mode 100644
index 0000000..1eee8b5
--- /dev/null
+++ b/frontend/assets/imgs/share.svg
@@ -0,0 +1,44 @@
+
+
+
diff --git a/frontend/assets/js/canvas.js b/frontend/assets/js/canvas.js
new file mode 100644
index 0000000..913644f
--- /dev/null
+++ b/frontend/assets/js/canvas.js
@@ -0,0 +1,436 @@
+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',
+ 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',
+ 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 initialisation(){
+ 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();
+ }
+ node_layer.draw();
+
+
+ edge_layer.show();
+
+}
+initialisation();
+//-----------------------------------------------------------------------------
+//------------------------------utils------------------------------------------
+//-----------------------------------------------------------------------------
+function sort(array) {
+ return array.sort(function(a, b) {
+ return a - b;
+ })
+}
+//-----------------------------------------------------------------------------
+//---------------------------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();
+}
+
+
+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();
+};
+
+
+
+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"});
+ }
+}
+
+
+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({
+ "start": selected[0].children[1].text(),
+ "end": selected[1].children[1].text(),
+ "weight": text
+ });
+ selected = [];
+ draw_selected_nodes();
+ },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);
+ }
+ });
+ });
+}
+
+function find_path(){
+ draw_path(elem);
+}
+
+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);
+ 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 = ((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)
+ }
+}, 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;};
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..d6659db
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,61 @@
+
+
+
+
+ Assignment
+
+
+
+
+
+
+ You have to select at least 2 nodes
+
+
+
+
+
+
+
+
diff --git a/main.py b/main.py
index b2d8d80..ce310df 100644
--- a/main.py
+++ b/main.py
@@ -2,6 +2,8 @@ from flask import Flask, jsonify, request, render_template
import numpy as np
app = Flask(__name__)
+app.template_folder = "frontend/"
+app.static_folder = "frontend/assets/"
class Graph(object):
diff --git a/templates/index.html b/templates/index.html
deleted file mode 100644
index 172f754..0000000
--- a/templates/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
- Assignment
-
-
- nodes: {{ nodes }}
- edges: {{ edges }}
-
-
-
-