From 17034b13e7e412dc35591338148223112d5f73e7 Mon Sep 17 00:00:00 2001 From: Alexander VanTol Date: Wed, 19 Jun 2019 19:57:37 -0500 Subject: [PATCH] fix client update issue, make grid size adjustable, refactoring --- README.md | 1 + app.py | 16 ++- config.py | 1 + static/main.css | 9 +- static/main.js | 289 +++++++++++++++++++++++++++++++------------ templates/index.html | 9 +- 6 files changed, 235 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 08b6b23..b5e80ad 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - [x] Can Create/Delete entire entities - [x] Simple navbar - [x] Any entity update (movement/attribute change) reflected on all clients using websockets +- [x] Adjustable grid size ## Milestone 2: Map Mode diff --git a/app.py b/app.py index cd06b4a..3dd82d7 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,10 @@ -from flask import Flask, render_template, jsonify +from flask import Flask, render_template, jsonify, make_response from flask_socketio import SocketIO, emit from mongoengine import * from mongo import Entity, DoesNotExist from config import config import json +import logging # Establishing a Connection connect("mongoengine_test", host="localhost", port=27017) @@ -12,10 +13,13 @@ app.config["SECRET_KEY"] = config.SECRET_KEY socketio = SocketIO(app) +logging.basicConfig(level=(logging.DEBUG if config.DEBUG else logging.INFO)) +logging.getLogger('flask_cors').level = logging.DEBUG @app.route("/") def index(): - return render_template("index.html") + response = make_response(render_template("index.html")) + return response @app.route("/api/entities", methods=["GET"]) @@ -26,7 +30,7 @@ def get_entities(): @socketio.on("delete entity", namespace="/test") def delete_entity(entity_id): Entity.objects(entity_id=entity_id).delete() - + logging.info(f"deleted entity {entity_id}") emit("deleted entity", entity_id, broadcast=True) @@ -35,7 +39,7 @@ def update_entity(data): data_dict = json.loads(data) entity_id = data_dict.get("attrs", {}).get("id", {}) if not entity_id: - print(f"error. {data_dict} has no 'attrs.id'") + logging.error(f"error. {data_dict} has no 'attrs.id'") return # "flatten", get attrs on first level and then other things @@ -59,6 +63,8 @@ def update_entity(data): Entity.objects(entity_id=entity_id).update(**new, upsert=True) entity = Entity.objects.get(entity_id=entity_id) + logging.info(f"emit updated entity {entity.to_json().get('id')}") + logging.debug(entity.to_json()) emit("updated entity", entity.to_json(), broadcast=True) @@ -69,7 +75,7 @@ def test_connect(): @socketio.on("disconnect", namespace="/test") def test_disconnect(): - print("Client disconnected") + logging.info("Client disconnected") if __name__ == "__main__": diff --git a/config.py b/config.py index a2134c5..f3bbc67 100644 --- a/config.py +++ b/config.py @@ -8,6 +8,7 @@ class Config(object): SECRET_KEY = os.environ.get("SECRET_KEY") or "you-will-never-guess" + DEBUG = os.environ.get("DEBUG") or True config = Config() diff --git a/static/main.css b/static/main.css index 920e4ca..d87a005 100644 --- a/static/main.css +++ b/static/main.css @@ -38,8 +38,10 @@ body { margin: 0; } -.navbar a:hover, .dropdown:hover .dropbtn, .dropbtn:focus { - background-color: red; +.navbar a:hover, +.dropdown:hover .dropbtn, +.dropbtn:focus { + /*background-color: red;*/ } .dropdown-content { @@ -47,7 +49,7 @@ body { position: absolute; background-color: #f9f9f9; min-width: 160px; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); z-index: 1; } @@ -76,6 +78,7 @@ body { border: 1px dotted gray; padding-right: 15px; } + .new-attribute-value { border: 1px dotted gray; padding-right: 15px; diff --git a/static/main.js b/static/main.js index b64df74..9a9d6a0 100644 --- a/static/main.js +++ b/static/main.js @@ -1,8 +1,12 @@ /* When the user clicks on the button, toggle between hiding and showing the dropdown content */ -function openNavDropdown() { +function openNavNewDropdown() { document.getElementById("navNewDropdown").classList.toggle("show"); } + +function openNavRemoveDropdown() { + document.getElementById("navRemoveDropdown").classList.toggle("show"); +} // Close the dropdown if the user clicks outside of it window.onclick = function(e) { if (!e.target.matches('.dropbtn')) { @@ -10,17 +14,21 @@ window.onclick = function(e) { if (navNewDropdown.classList.contains('show')) { navNewDropdown.classList.remove('show'); } + var navRemoveDropdown = document.getElementById("navRemoveDropdown"); + if (navRemoveDropdown.classList.contains('show')) { + navRemoveDropdown.classList.remove('show'); + } } } -function newEntity(raw_entity, layer, stage) { +function createNewEntity(raw_entity, layer, stage) { let entity; if (raw_entity.className == "Rect") { let entity_values; try { - entity_values = getEntityValues(raw_entity); + entity_values = convertToEntityAttributes(raw_entity); } catch (err) { - console.log(`Could not create new entity. Error: ${err}`); + console.log(`could not create new entity. error: ${err}`); return; } entity = new Konva.Rect(entity_values); @@ -29,25 +37,32 @@ function newEntity(raw_entity, layer, stage) { return; } setEntityEventHandling(entity, stage, layer); + layer.add(entity); layer.draw(); + + return entity; } -function updateEntity(raw_entity) { - let entity_id = raw_entity.id; - console.log(`updating ${entity_id}`); - var shape = STAGE.find(`#${entity_id}`)[0]; - if (shape) { +function updateLocalEntity(raw_entity) { + let entity_attrs = convertToEntityAttributes(raw_entity); + let entity_id = entity_attrs.id; + var entity = STAGE.find(`#${entity_id}`)[0]; + if (entity) { + console.log(`updating ${entity_id}`); try { - shape.attrs = getEntityValues(raw_entity); - let newPosition = getSnappedPosition(shape.x(), shape.y()) - shape.x(newPosition.x); - shape.y(newPosition.y); - STAGE.draw(); + entity.attrs = entity_attrs; + let newPosition = getSnappedPosition(entity.x(), entity.y()) + entity.x(newPosition.x); + entity.y(newPosition.y); } catch (err) { - console.log(`Could not update ${entity_id}. Error: ${err}`); + console.log(`could not update ${entity_id}. error: ${err}`); } + } else { + console.log(`could not update ${entity_id}. could not find on stage.`); } + + return entity; } function saveEntity(entity_id, layer) { @@ -62,15 +77,36 @@ function saveEntity(entity_id, layer) { attributes[attributeName] = attributeValue; } }); - updateEntity(getEntityValues(attributes)); + updateLocalEntity(attributes); layer.draw(); - socket.emit('update entity', entity); + sendEntityUpdate(entity); +} + +function deleteLocalEntities() { + MAIN_LAYER.destroyChildren(); +} + +function deleteLocalEntitiesUpdateRemote() { + var entities = MAIN_LAYER.getChildren().each(function(entity, n) { + var entity_id = entity.attrs.id; + if (entity_id) { + socket.emit('delete entity', `${entity_id}`); + } + }); +} + +function sendEntityUpdate(entity) { + var raw_entity = convertLocalEntityToRemote(entity); + console.log('sending update with entity:'); + console.log(raw_entity); + socket.emit('update entity', JSON.stringify(raw_entity)); } function newDefaultRectangle() { // create in top left - var newPosition = getSnappedPosition(STAGE_X + 50, STAGE_Y + 50) - newRectangle(newPosition.x, newPosition.y, MAIN_LAYER, STAGE); + var newPosition = getSnappedPosition(STAGE_X + GRID_BLOCK_SIZE, STAGE_Y + GRID_BLOCK_SIZE) + var rectangle = newRectangle(newPosition.x, newPosition.y, MAIN_LAYER, STAGE); + sendEntityUpdate(rectangle); } function newRectangle(x, y, layer, stage, entity_id = null) { @@ -79,8 +115,8 @@ function newRectangle(x, y, layer, stage, entity_id = null) { id: entity_id, x: x, y: y, - width: blockSnapSize * 1, - height: blockSnapSize * 1, + width: GRID_BLOCK_SIZE * 1, + height: GRID_BLOCK_SIZE * 1, fill: '#fff', stroke: '#ddd', strokeWidth: 1, @@ -93,6 +129,8 @@ function newRectangle(x, y, layer, stage, entity_id = null) { setEntityEventHandling(rectangle, stage, layer); layer.add(rectangle); stage.draw(); + + return rectangle; } function setEntityEventHandling(entity, stage, layer) { @@ -105,14 +143,16 @@ function setEntityEventHandling(entity, stage, layer) { entity.on('dragend', (e) => { entity.x(SHADOW_RECT.x()); entity.y(SHADOW_RECT.y()); + layer.draw(); stage.draw(); SHADOW_RECT.hide(); - socket.emit('update entity', entity); + sendEntityUpdate(entity); }); entity.on('dragmove', (e) => { let newPosition = getSnappedPosition(entity.x(), entity.y()) SHADOW_RECT.x(newPosition.x); SHADOW_RECT.y(newPosition.y); + layer.draw(); stage.draw(); }); entity.on('click', (e) => { @@ -124,7 +164,7 @@ function setEntityEventHandling(entity, stage, layer) { `; $("#dialogs").append(content); - socket.emit('update entity', entity); + sendEntityUpdate(entity); $(`#${entity_id}`).dialog({ buttons: [{ text: "Delete", @@ -152,6 +192,45 @@ function setEntityEventHandling(entity, stage, layer) { }); } +function adjustGridSize() { + // TODO take user input + // http://api.jqueryui.com/dialog/#option-modal + var modalUUID = uuidv4(); + let content = ` +
+

Current grid size: ${GRID_BLOCK_SIZE}

+

Enter new grid size:

+
+`; + $("#dialogs").append(content); + + $(`#${modalUUID}`).dialog({ + modal: true, + draggable: false, + buttons: [{ + text: "Save", + icon: "ui-icon-disk", + click: function() { + var newSize = Math.abs( + getNumber($(`#${modalUUID}`).find('.new-grid-size').val()) + ); + + if (newSize) { + GRID_BLOCK_SIZE = newSize; + } + + // snap to new positions + deleteLocalEntities(); + recomputeGlobals(); + recreateGridLayer(); + createLocalEntitiesFromRemote(); + + $(`#${modalUUID}`).dialog('close'); + } + }] + }); +} + // https://stackoverflow.com/a/2117523 function uuidv4() { return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => @@ -159,55 +238,63 @@ function uuidv4() { ) } -function get_number(value) { +function getNumber(value) { if (isNaN(value)) throw `${value} not a number`; return Number(value); } -function getEntityValues(raw_entity) { +function convertToEntityAttributes(raw_entity) { + if (raw_entity.attrs) { + given_attrs = raw_entity.attrs + } else { + given_attrs = raw_entity + } + + var entity_attrs = JSON.parse(JSON.stringify(raw_entity)); + if (raw_entity.className == "Rect") { - for (const key of Object.keys(raw_entity)) { + for (const key of Object.keys(given_attrs)) { if (key == "draggable") { - raw_entity.draggable = (raw_entity.draggable === true || raw_entity.draggable == 'true'); + entity_attrs.draggable = (given_attrs.draggable === true || given_attrs.draggable == 'true'); } else if (key == "x") { - raw_entity.x = get_number(raw_entity.x); + entity_attrs.x = getNumber(given_attrs.x) * GRID_BLOCK_SIZE; } else if (key == "y") { - raw_entity.y = get_number(raw_entity.y); + entity_attrs.y = getNumber(given_attrs.y) * GRID_BLOCK_SIZE; } else if (key == "height") { - raw_entity.height = get_number(raw_entity.height); + entity_attrs.height = getNumber(given_attrs.height) * GRID_BLOCK_SIZE; } else if (key == "width") { - raw_entity.width = get_number(raw_entity.width); + entity_attrs.width = getNumber(given_attrs.width) * GRID_BLOCK_SIZE; } else if (key == "shadowBlur") { - raw_entity.shadowBlur = get_number(raw_entity.shadowBlur); + entity_attrs.shadowBlur = getNumber(given_attrs.shadowBlur); } else if (key == "shadowOffsetX") { - raw_entity.shadowOffsetX = get_number(raw_entity.shadowOffsetX); + entity_attrs.shadowOffsetX = getNumber(given_attrs.shadowOffsetX); } else if (key == "shadowOffsetY") { - raw_entity.shadowOffsetY = get_number(raw_entity.shadowOffsetY); + entity_attrs.shadowOffsetY = getNumber(given_attrs.shadowOffsetY); } else if (key == "shadowOpacity") { - raw_entity.shadowOpacity = get_number(raw_entity.shadowOpacity); + entity_attrs.shadowOpacity = getNumber(given_attrs.shadowOpacity); } else if (key == "strokeWidth") { - raw_entity.strokeWidth = get_number(raw_entity.strokeWidth); + entity_attrs.strokeWidth = getNumber(given_attrs.strokeWidth); } else if (key == "offsetX") { - raw_entity.offsetX = get_number(raw_entity.offsetX); + entity_attrs.offsetX = getNumber(given_attrs.offsetX); } else if (key == "offsetY") { - raw_entity.offsetY = get_number(raw_entity.offsetY); + entity_attrs.offsetY = getNumber(given_attrs.offsetY); } else if (key == "rotation") { - raw_entity.rotation = get_number(raw_entity.rotation); + entity_attrs.rotation = getNumber(given_attrs.rotation); } else if (key == "scaleX") { - raw_entity.scaleX = get_number(raw_entity.scaleX); + entity_attrs.scaleX = getNumber(given_attrs.scaleX); } else if (key == "scaleY") { - raw_entity.scaleY = get_number(raw_entity.scaleY); + entity_attrs.scaleY = getNumber(given_attrs.scaleY); } else if (key == "skewX") { - raw_entity.skewX = get_number(raw_entity.skewX); + entity_attrs.skewX = getNumber(given_attrs.skewX); } else if (key == "skewY") { - raw_entity.skewY = get_number(raw_entity.skewY); + entity_attrs.skewY = getNumber(given_attrs.skewY); } else { // unknown values default to string - raw_entity[key] = raw_entity[key].toString() + entity_attrs[key] = given_attrs[key].toString() } } } - return raw_entity + return entity_attrs } function mod(n, m) { @@ -218,7 +305,7 @@ function mod(n, m) { function createGridLayer() { var gridLayer = new Konva.Layer(); - for (var i = START_X; i < STAGE_MAX_X; i += blockSnapSize) { + for (var i = START_X; i < STAGE_MAX_X; i += GRID_BLOCK_SIZE) { gridLayer.add(new Konva.Line({ points: [i, STAGE_Y, i, STAGE_MAX_Y], stroke: '#ddd', @@ -226,7 +313,7 @@ function createGridLayer() { selectable: false })); } - for (var j = START_Y; j < STAGE_MAX_Y; j += blockSnapSize) { + for (var j = START_Y; j < STAGE_MAX_Y; j += GRID_BLOCK_SIZE) { gridLayer.add(new Konva.Line({ points: [STAGE_X, j, STAGE_MAX_X, j], stroke: '#ddd', @@ -240,21 +327,53 @@ function createGridLayer() { } function getSnappedPosition(x, y) { - var xRem = mod(x, blockSnapSize); - var yRem = mod(y, blockSnapSize); - if (xRem <= blockSnapSize / 2) { + var xRem = mod(x, GRID_BLOCK_SIZE); + var yRem = mod(y, GRID_BLOCK_SIZE); + if (xRem <= GRID_BLOCK_SIZE / 2) { var newX = x - xRem; } else { - var newX = x + (blockSnapSize - xRem); + var newX = x + (GRID_BLOCK_SIZE - xRem); } - if (yRem <= blockSnapSize / 2) { + if (yRem <= GRID_BLOCK_SIZE / 2) { var newY = y - yRem; } else { - var newY = y + (blockSnapSize - yRem); + var newY = y + (GRID_BLOCK_SIZE - yRem); } return { "x": newX, "y": newY } } +function convertLocalEntityToRemote(entity) { + // idk why but one JSON.parse resulted in a string... not a full JSON object + var raw_entity = JSON.parse(JSON.parse(JSON.stringify(entity))); + + // store the location and size in terms of the grid size on he server + var grid_location_x = (raw_entity.attrs.x / GRID_BLOCK_SIZE) || 0; + var grid_location_y = (raw_entity.attrs.y / GRID_BLOCK_SIZE) || 0; + var grid_height = (raw_entity.attrs.height / GRID_BLOCK_SIZE) || 0; + var grid_width = (raw_entity.attrs.width / GRID_BLOCK_SIZE) || 0; + + raw_entity.attrs.x = grid_location_x; + raw_entity.attrs.y = grid_location_y; + raw_entity.attrs.height = grid_height; + raw_entity.attrs.width = grid_width; + + return raw_entity; +} + +function createLocalEntitiesFromRemote() { + $.getJSON("/api/entities", function(response) { + for (var i = 0; i < response.entities.length; i++) { + createNewEntity(response.entities[i], MAIN_LAYER, STAGE); + } + }); +} + +function recreateGridLayer() { + var newGridLayer = createGridLayer(STAGE); + GRID_LAYER.destroy(); + GRID_LAYER = newGridLayer; +} + function recomputeGlobals() { WIDTH = $(window).width(); HEIGHT = $(window).height(); @@ -262,33 +381,32 @@ function recomputeGlobals() { 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)); + START_X = STAGE_X + (GRID_BLOCK_SIZE - mod(STAGE_X, GRID_BLOCK_SIZE)); + START_Y = STAGE_Y + (GRID_BLOCK_SIZE - mod(STAGE_Y, GRID_BLOCK_SIZE)); } var WIDTH = $(window).width(); var HEIGHT = $(window).height(); -var shadowOffset = 20; -var tween = null; -var blockSnapSize = 50; +var GRID_BLOCK_SIZE = 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(); + var entity = STAGE.find(`#${entity_id}`)[0]; + if (entity) { + console.log(`deleting ${entity_id}`); + entity.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 entity = updateLocalEntity(data); + + // if didn't get updated, it doesn't exist + if (!entity) { + entity = createNewEntity(data, MAIN_LAYER, STAGE); } + var entity_id = entity.attrs.id; var keys = Object.keys(data); ul = $(`