Infobox and added save+clear

delete_edge
Ugo Finnendahl 5 years ago
parent ed5e9e11d9
commit abc08e2b97
  1. 21
      frontend/assets/css/src/style.scss
  2. 2
      frontend/assets/css/style.min.css
  3. 163
      frontend/assets/js/canvas.js
  4. 11
      frontend/index.html
  5. 68
      main.py

@ -12,7 +12,7 @@ main{
position: relative;
height: 100vh;
#infobar{
#errorbar{
position: absolute;
top:0;
width: 100%;
@ -106,5 +106,24 @@ main{
left: auto;
bottom: 1em;
right: 1em;
left: 1em;
display: flex;
justify-content: flex-end;
#infobar{
position: absolute;
bottom:0;
left: 0;
width: 50%;
padding: 0.4em;
color: rgba(0, 0, 0, 0.8);
font-weight: bold;
font-size: 1.8em;
z-index: 1;
opacity: 0;
transition: opacity 1s;
&.active{
opacity: 1;
}
}
}
}

@ -1 +1 @@
*{margin:0;padding:0;box-sizing:border-box;font-family:"Roboto-Regular"}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:200px;font-size:1.2em;text-align:center;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;font-family:"Roboto-Regular"}main{position:relative;height:100vh}main #errorbar{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 #errorbar.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:200px;font-size:1.2em;text-align:center;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;left:1em;display:flex;justify-content:flex-end}main footer #infobar{position:absolute;bottom:0;left:0;width:50%;padding:0.4em;color:rgba(0,0,0,0.8);font-weight:bold;font-size:1.8em;z-index:1;opacity:0;transition:opacity 1s}main footer #infobar.active{opacity:1}

@ -28,32 +28,32 @@ const node_layout = {
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,
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,
...text_layout,
width: 2 * node_size,
height: 2 * node_size,
fontFamily: 'Roboto, Arial, Verdana, sans-serif',
fill: 'black',
verticalAlign: 'middle',
align: 'center'
};
@ -118,8 +118,8 @@ function sort(array) {
})
}
function show_error(msg) {
let infobar = document.querySelector("#infobar");
function show_info(msg, elem="#errorbar") {
let infobar = document.querySelector(elem);
infobar.innerHTML = msg;
infobar.classList.add("active");
window.setTimeout(function() {
@ -127,7 +127,6 @@ function show_error(msg) {
}, 4000);
}
//-----------------------------------------------------------------------------
//---------------------------draw methods--------------------------------------
//-----------------------------------------------------------------------------
@ -173,7 +172,6 @@ function draw_new_node(name, x, y) {
node_layer.draw();
}
function draw_new_edge(edge) {
let start_node = stage.findOne('#' + edge["start"]);
let end_node = stage.findOne('#' + edge["end"]);
@ -264,6 +262,8 @@ function draw_path(path) {
path_elements[id].children[0].strokeWidth(node_layout.strokeWidth + 1);
path_elements[id].children[0].stroke("rgb(240, 141, 25)");
}
node_layer.draw();
edge_layer.draw();
}
function on_node_click(e) {
@ -281,19 +281,58 @@ function on_node_click(e) {
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 post(endpoint, data) {
return fetch(url + endpoint, {
method: 'POST',
body: JSON.stringify(data),
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";
@ -303,12 +342,12 @@ function create_node() {
y: (Math.random()-0.5)*node_size*4+height / 2
};
create_input(pos.x, pos.y).then(function(text) {
post("nodes", {
rest_req('POST', "nodes", {
name: text
})
.then(function(res) {
if (res["error"]) {
show_error(res["error"])
show_info(res["error"])
} else {
draw_new_node(text, pos.x, pos.y);
}
@ -321,26 +360,22 @@ function create_node() {
function create_edge() {
if (selected.length < 2) {
show_error("Select at least 2 nodes.");
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) {
post("edges", {
rest_req('POST',"edges", {
start: selected[0].id(),
end: selected[1].id(),
weight: text
})
.then(function(res) {
if (res["error"]) {
show_error(res["error"])
show_info(res["error"])
} else {
draw_new_edge({
"start": selected[0].children[1].text(),
"end": selected[1].children[1].text(),
"weight": text
});
draw_new_edge(res);
selected = [];
draw_selected_nodes();
}
@ -350,56 +385,51 @@ function create_edge() {
}, 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.");
show_info("Select 2 nodes.");
return;
}
fetch(url + "paths/" + data[0].attrs.id + "/" + data[1].attrs.id).then(res => res.json())
rest_req("GET", "paths/" + data[0].attrs.id + "/" + data[1].attrs.id, null)
.then(function(res) {
if (res["error"]) {
show_error(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 {
console.log(res);
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 {
location.reload();
}
})
.catch(error => console.error('Error:', error));
}
//-----------------------------------------------------------------------------
//------------------------background methods-----------------------------------
@ -507,8 +537,9 @@ var anim = new Konva.Animation(function(frame) {
}, node_layer);
anim.start();
initialization();
// bug: this avoids glitches
window.onfocus = function() {focus = true;};
window.onblur = function() {focus = false;};
initialization();

@ -10,7 +10,7 @@
</head>
<body>
<main>
<div id="infobar">You have to select at least 2 nodes</div>
<div id="errorbar">You have to select at least 2 nodes</div>
<div id="canvas" class="canvas"></div>
<nav>
<button type="button" onclick="create_node()">
@ -27,6 +27,13 @@
</button>
</nav>
<footer>
<div id="infobar"></div>
<button type="button" onclick="save_graph()">
<h1>Save Graph</h1>
</button>
<button type="button" onclick="clear_graph()">
<h1>Clear Graph</h1>
</button>
<button type="button" data-activeon="auto_layout" onclick="toggle_auto_layout()">
<h1>Automatic Layout</h1>
</button>
@ -35,7 +42,7 @@
<script>
var nodes = {{ nodes|safe }};
var edges = {{ edges|safe }};
const url = "http://{{ request.host|safe }}/api/";
const url = "http://{{ request.host|safe }}/";
</script>
<script src="{{ url_for('static', filename='js/canvas.js')}}"></script>
</body>

@ -1,24 +1,27 @@
import json, os
from flask import Flask, jsonify, request, render_template, escape
import numpy as np
app = Flask(__name__)
app.template_folder = "frontend/"
app.static_folder = "frontend/assets/"
PATH = "graph.json"
graph = {}
graph = {
# "A": {"B":10},
# "B": {"E":10, "D":10, "C":10, "A":10},
# "C": {"B":10, "D":10},
# "D": {"B":10, "C":10},
# "E": {"B":10}
}
def bellmann_ford(graph, node_a):
"""
Calculate distance from node_a to all other nodes assuming no negative
cycles.
def bellmann_ford(graph, node_a, node_b):
"""Calculate distance from node_a to node_b assuming no negative cycles."""
Kommentar:
Auch wenn Dijkstra der effizientere Algorithmus in diesem Falle wäre,
habe mich dazu entschieden Bellmann Ford zu implementieren.
Dijkstra musste ich schon mindestens 4 mal in meiner Ausbildung
implementieren. Ich hatte mal wieder Lust auf Dynamische Programmierung.
"""
# weights
from_a_to = {node: np.inf for node in graph}
from_a_to = {node: float("inf") for node in graph}
from_a_to[node_a] = 0
# save path so we dont have to backtrace later
path_from_a_to = {node: [] for node in graph}
@ -27,9 +30,9 @@ def bellmann_ford(graph, node_a, node_b):
for i in range(len(graph.keys())-1):
for s,e in [(s,e) for s in graph for e in graph[s]]:
if from_a_to[s] + graph[s][e] < from_a_to[e]:
path_from_a_to[e] = path_from_a_to[s] + [e]
from_a_to[e] = from_a_to[s] + graph[s][e]
return path_from_a_to[node_b]
path_from_a_to[e] = path_from_a_to[s] + [e]
from_a_to[e] = from_a_to[s] + graph[s][e]
return path_from_a_to
def error(message):
@ -47,6 +50,9 @@ def edges_to_json(graph):
return return_list
# -----------------------------------------------------------------------------
# ------------------------------API--------------------------------------------
# -----------------------------------------------------------------------------
@app.route('/api/paths/<start>/<end>', methods=['GET'])
def get_path(start, end):
if start == end:
@ -56,23 +62,13 @@ def get_path(start, end):
if end not in graph:
return error("End node does not exist."), 400
path = bellmann_ford(graph, start, end)
path = bellmann_ford(graph, start)[end]
if len(path) == 0:
return error("There exists no path."), 400
return jsonify({'path': path})
@app.route('/api/nodes', methods=['GET'])
def get_nodes():
return jsonify({'nodes': graph.keys()})
@app.route('/api/edges', methods=['GET'])
def get_edges():
return jsonify({'edges': edges_to_json(graph)})
@app.route('/api/nodes', methods=['POST'])
def create_node():
if not request.json:
@ -126,10 +122,30 @@ def create_edge():
return jsonify(edge), 201
@app.route("/save")
def save():
try:
with open(PATH, 'w') as f:
json.dump(graph, f)
return jsonify({"info":"Saved graph",**graph}), 200
except Exception:
return error("Internal Server Error"), 500
@app.route("/clear")
def clear():
global graph
graph = {}
return "", 204
@app.route("/")
def index():
return render_template("index.html", nodes=list(graph.keys()), edges=edges_to_json(graph))
if __name__ == '__main__':
app.run(host="0.0.0.0", debug=True, port=5000)
if os.path.isfile(PATH) and os.access(PATH, os.R_OK):
with open(PATH, 'r') as f:
graph = json.load(f)
app.run(host="127.0.0.1", debug= True, port=5000)

Loading…
Cancel
Save