From 1cbb19e21086f981f98419e26f343f69d91c5a30 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Sat, 15 Jun 2019 14:25:48 -0500 Subject: [PATCH] cleanup/refactor/format --- README.md | 14 +- main.py => app.py | 28 ++- config.py | 4 +- mongo.py | 17 +- static/main.js | 346 ++++++++++++++++++++++++++++++ templates/index.html | 490 ++----------------------------------------- 6 files changed, 398 insertions(+), 501 deletions(-) rename main.py => app.py (74%) create mode 100644 static/main.js diff --git a/README.md b/README.md index 05b3ccc..08b6b23 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,12 @@ ## Milestone 1: Grid and Simple Blocks -* Draggable grid and movable entities that snap to grid -* Entities expose attributes used for drawing on canvas -* Can CRUD entity attributes - * Rudimentary dialog box with editable fields -* Can Create/Delete entire entities -* Simple navbar -* Any entity update (movement/attribute change) reflected on all clients - * using websockets +- [x] Draggable grid and movable entities that snap to grid +- [x] Entities expose attributes used for drawing on canvas (color, location, etc.) +- [x] Can CRUD entity attributes (dialog box with editable fields) +- [x] Can Create/Delete entire entities +- [x] Simple navbar +- [x] Any entity update (movement/attribute change) reflected on all clients using websockets ## Milestone 2: Map Mode diff --git a/main.py b/app.py similarity index 74% rename from main.py rename to app.py index c0f8b41..cd06b4a 100644 --- a/main.py +++ b/app.py @@ -1,7 +1,7 @@ from flask import Flask, render_template, jsonify from flask_socketio import SocketIO, emit from mongoengine import * -from mongo import Entity +from mongo import Entity, DoesNotExist from config import config import json @@ -12,13 +12,16 @@ app.config["SECRET_KEY"] = config.SECRET_KEY socketio = SocketIO(app) + @app.route("/") def index(): return render_template("index.html") -@app.route("/api/entities", methods=['GET']) + +@app.route("/api/entities", methods=["GET"]) def get_entities(): - return jsonify({ "entities": [entity.to_dict() for entity in Entity.objects.all()] }) + return jsonify({"entities": [entity.to_dict() for entity in Entity.objects.all()]}) + @socketio.on("delete entity", namespace="/test") def delete_entity(entity_id): @@ -42,23 +45,32 @@ def update_entity(data): # get current fields to diff against fields provided # any missing fields will be removed by using mongo's "unset" option - current = Entity.objects.get(entity_id=entity_id).to_json() - current.pop("id") - to_remove = {"unset__{}".format(key):1 for key in set(current.keys()) - set(new.keys())} - new.update(to_remove) + try: + current = Entity.objects.get(entity_id=entity_id).to_json() + current.pop("id") + to_remove = { + "unset__{}".format(key): 1 for key in set(current.keys()) - set(new.keys()) + } + new.update(to_remove) + except DoesNotExist: + # no existing record, so create everything + pass Entity.objects(entity_id=entity_id).update(**new, upsert=True) entity = Entity.objects.get(entity_id=entity_id) emit("updated entity", entity.to_json(), broadcast=True) + @socketio.on("connect", namespace="/test") def test_connect(): emit("connected", {"data": "Connected"}) + @socketio.on("disconnect", namespace="/test") def test_disconnect(): print("Client disconnected") + if __name__ == "__main__": - socketio.run(app) \ No newline at end of file + socketio.run(app) diff --git a/config.py b/config.py index 91416be..a2134c5 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,5 @@ import os + # from dotenv import load_dotenv basedir = os.path.abspath(os.path.dirname(__file__)) @@ -8,4 +9,5 @@ class Config(object): SECRET_KEY = os.environ.get("SECRET_KEY") or "you-will-never-guess" -config = Config() \ No newline at end of file + +config = Config() diff --git a/mongo.py b/mongo.py index 7ef593c..e5d7201 100644 --- a/mongo.py +++ b/mongo.py @@ -5,17 +5,10 @@ # Defining a Document class Entity(DynamicDocument): - entity_id = StringField( - required=True, unique=True, - default=str(uuid.uuid4()) - ) + entity_id = StringField(required=True, unique=True, default=str(uuid.uuid4())) def to_dict(self): - output = { - key:value - for key, value in - self.to_mongo().to_dict().items() - } + output = {key: value for key, value in self.to_mongo().to_dict().items()} # return an "id" field from the entity_id field del output["_id"] output["id"] = output["entity_id"] @@ -24,11 +17,7 @@ def to_dict(self): return output def to_json(self): - output = { - key:value - for key, value in - json.loads(super().to_json()).items() - } + output = {key: value for key, value in json.loads(super().to_json()).items()} # return an "id" field from the entity_id field del output["_id"] output["id"] = output["entity_id"] diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..b64df74 --- /dev/null +++ b/static/main.js @@ -0,0 +1,346 @@ +/* When the user clicks on the button, +toggle between hiding and showing the dropdown content */ +function openNavDropdown() { + document.getElementById("navNewDropdown").classList.toggle("show"); +} +// Close the dropdown if the user clicks outside of it +window.onclick = function(e) { + if (!e.target.matches('.dropbtn')) { + var navNewDropdown = document.getElementById("navNewDropdown"); + if (navNewDropdown.classList.contains('show')) { + navNewDropdown.classList.remove('show'); + } + } +} + +function newEntity(raw_entity, layer, stage) { + let entity; + if (raw_entity.className == "Rect") { + let entity_values; + try { + entity_values = getEntityValues(raw_entity); + } catch (err) { + console.log(`Could not create new entity. Error: ${err}`); + return; + } + entity = new Konva.Rect(entity_values); + } else { + console.log(`could not determine class for ${raw_entity}`); + return; + } + setEntityEventHandling(entity, stage, layer); + layer.add(entity); + layer.draw(); +} + +function updateEntity(raw_entity) { + let entity_id = raw_entity.id; + console.log(`updating ${entity_id}`); + var shape = STAGE.find(`#${entity_id}`)[0]; + if (shape) { + try { + shape.attrs = getEntityValues(raw_entity); + let newPosition = getSnappedPosition(shape.x(), shape.y()) + shape.x(newPosition.x); + shape.y(newPosition.y); + STAGE.draw(); + } catch (err) { + console.log(`Could not update ${entity_id}. Error: ${err}`); + } + } +} + +function saveEntity(entity_id, layer) { + console.log(`saving ${entity_id}...`); + var entity = STAGE.find(`#${entity_id}`)[0]; + var attributes = {}; + var attributesHtml = $(`#${entity_id}`).find('li'); + attributesHtml.each(function() { + attributeName = $(this).find('.attribute')[0].innerText.replace(" ", "").replace(":", ""); + attributeValue = $(this).find('.attribute-value')[0].innerText.replace(" ", "").replace(":", ""); + if (attributeName) { + attributes[attributeName] = attributeValue; + } + }); + updateEntity(getEntityValues(attributes)); + layer.draw(); + socket.emit('update entity', entity); +} + +function newDefaultRectangle() { + // create in top left + var newPosition = getSnappedPosition(STAGE_X + 50, STAGE_Y + 50) + newRectangle(newPosition.x, newPosition.y, MAIN_LAYER, STAGE); +} + +function newRectangle(x, y, layer, stage, entity_id = null) { + entity_id = entity_id || uuidv4(); + let rectangle = new Konva.Rect({ + id: entity_id, + x: x, + y: y, + width: blockSnapSize * 1, + height: blockSnapSize * 1, + fill: '#fff', + stroke: '#ddd', + strokeWidth: 1, + shadowColor: 'black', + shadowBlur: 1, + shadowOffset: { x: 1, y: 1 }, + shadowOpacity: 0.2, + draggable: true + }); + setEntityEventHandling(rectangle, stage, layer); + layer.add(rectangle); + stage.draw(); +} + +function setEntityEventHandling(entity, stage, layer) { + let entity_id = entity.attrs.id; + entity.on('dragstart', (e) => { + SHADOW_RECT.show(); + SHADOW_RECT.moveToTop(); + entity.moveToTop(); + }); + entity.on('dragend', (e) => { + entity.x(SHADOW_RECT.x()); + entity.y(SHADOW_RECT.y()); + stage.draw(); + SHADOW_RECT.hide(); + socket.emit('update entity', entity); + }); + entity.on('dragmove', (e) => { + let newPosition = getSnappedPosition(entity.x(), entity.y()) + SHADOW_RECT.x(newPosition.x); + SHADOW_RECT.y(newPosition.y); + stage.draw(); + }); + entity.on('click', (e) => { + if (e.evt.button === 2) { + if (!$(`div[aria-describedby='${entity_id}']`).length) { + let content = ` +
+

This is the default dialog ${entity_id}

+
+`; + $("#dialogs").append(content); + socket.emit('update entity', entity); + $(`#${entity_id}`).dialog({ + buttons: [{ + text: "Delete", + icon: "ui-icon-trash", + // showText: false, + click: function() { + socket.emit('delete entity', `${entity_id}`); + $(`#${entity_id}`).dialog("destroy"); + } + }, + { + text: "Save", + icon: "ui-icon-disk", + // showText: false, + click: function() { + saveEntity(entity_id, layer); + } + } + ] + }); + } + $(`#${entity_id}`).dialog('open'); + $(`#${entity_id}`).dialog("option", "position", { my: "right top", at: "right-5% top+10%", of: window }); + } + }); +} + +// https://stackoverflow.com/a/2117523 +function uuidv4() { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ) +} + +function get_number(value) { + if (isNaN(value)) throw `${value} not a number`; + return Number(value); +} + +function getEntityValues(raw_entity) { + if (raw_entity.className == "Rect") { + for (const key of Object.keys(raw_entity)) { + if (key == "draggable") { + raw_entity.draggable = (raw_entity.draggable === true || raw_entity.draggable == 'true'); + } else if (key == "x") { + raw_entity.x = get_number(raw_entity.x); + } else if (key == "y") { + raw_entity.y = get_number(raw_entity.y); + } else if (key == "height") { + raw_entity.height = get_number(raw_entity.height); + } else if (key == "width") { + raw_entity.width = get_number(raw_entity.width); + } else if (key == "shadowBlur") { + raw_entity.shadowBlur = get_number(raw_entity.shadowBlur); + } else if (key == "shadowOffsetX") { + raw_entity.shadowOffsetX = get_number(raw_entity.shadowOffsetX); + } else if (key == "shadowOffsetY") { + raw_entity.shadowOffsetY = get_number(raw_entity.shadowOffsetY); + } else if (key == "shadowOpacity") { + raw_entity.shadowOpacity = get_number(raw_entity.shadowOpacity); + } else if (key == "strokeWidth") { + raw_entity.strokeWidth = get_number(raw_entity.strokeWidth); + } else if (key == "offsetX") { + raw_entity.offsetX = get_number(raw_entity.offsetX); + } else if (key == "offsetY") { + raw_entity.offsetY = get_number(raw_entity.offsetY); + } else if (key == "rotation") { + raw_entity.rotation = get_number(raw_entity.rotation); + } else if (key == "scaleX") { + raw_entity.scaleX = get_number(raw_entity.scaleX); + } else if (key == "scaleY") { + raw_entity.scaleY = get_number(raw_entity.scaleY); + } else if (key == "skewX") { + raw_entity.skewX = get_number(raw_entity.skewX); + } else if (key == "skewY") { + raw_entity.skewY = get_number(raw_entity.skewY); + } else { + // unknown values default to string + raw_entity[key] = raw_entity[key].toString() + } + } + } + return raw_entity +} + +function mod(n, m) { + // js modulo operator (%) is weird with negative numbers + // https://stackoverflow.com/a/17323608 + return ((n % m) + m) % m; +} + +function createGridLayer() { + var gridLayer = new Konva.Layer(); + for (var i = START_X; i < STAGE_MAX_X; i += blockSnapSize) { + gridLayer.add(new Konva.Line({ + points: [i, STAGE_Y, i, STAGE_MAX_Y], + stroke: '#ddd', + strokeWidth: 1, + selectable: false + })); + } + for (var j = START_Y; j < STAGE_MAX_Y; j += blockSnapSize) { + gridLayer.add(new Konva.Line({ + points: [STAGE_X, j, STAGE_MAX_X, j], + stroke: '#ddd', + strokeWidth: 1, + selectable: false + })); + } + STAGE.add(gridLayer); + gridLayer.moveToBottom(); + return gridLayer; +} + +function getSnappedPosition(x, y) { + var xRem = mod(x, blockSnapSize); + var yRem = mod(y, blockSnapSize); + if (xRem <= blockSnapSize / 2) { + var newX = x - xRem; + } else { + var newX = x + (blockSnapSize - xRem); + } + if (yRem <= blockSnapSize / 2) { + var newY = y - yRem; + } else { + var newY = y + (blockSnapSize - yRem); + } + return { "x": newX, "y": newY } +} + +function recomputeGlobals() { + WIDTH = $(window).width(); + HEIGHT = $(window).height(); + STAGE_X = -STAGE.attrs.x || 0; + STAGE_Y = -STAGE.attrs.y || 0; + STAGE_MAX_X = STAGE_X + WIDTH; + STAGE_MAX_Y = STAGE_Y + HEIGHT; + START_X = STAGE_X + (blockSnapSize - mod(STAGE_X, blockSnapSize)); + START_Y = STAGE_Y + (blockSnapSize - mod(STAGE_Y, blockSnapSize)); +} + +var WIDTH = $(window).width(); +var HEIGHT = $(window).height(); +var shadowOffset = 20; +var tween = null; +var blockSnapSize = 50; +var socket = io.connect('http://' + document.domain + ':' + location.port + '/test'); +socket.on('deleted entity', function(data) { + let entity_id = data; + console.log(`deleting ${entity_id}`); + var shape = STAGE.find(`#${entity_id}`)[0]; + shape.destroy(); + STAGE.draw(); +}); +socket.on('updated entity', function(data) { + let entity_id = data.id; + console.log(`updating ${entity_id}`); + var shape = STAGE.find(`#${entity_id}`)[0]; + if (shape) { + shape.attrs = data; + shape.x(shape.attrs.x); + shape.y(shape.attrs.y); + MAIN_LAYER.draw(); + } + var keys = Object.keys(data); + ul = $(`