From 0339962f3cbdc07ccadc6accb5482ffa416115f7 Mon Sep 17 00:00:00 2001 From: Christian Franke Date: Thu, 12 Dec 2013 13:15:52 +0100 Subject: Somewhat working, cherrypy's worker threads can't keep up, though :/ --- .gitignore | 1 + README | 29 +++++++ run_server.py | 136 ++++++++++++++++++++++++++++++++ web/css/leaflet.label.css | 52 +++++++++++++ web/js/eventmap.js | 194 ++++++++++++++++++++++++++++++++++++++-------- web/js/leaflet.label.js | 9 +++ 6 files changed, 387 insertions(+), 34 deletions(-) create mode 100644 .gitignore create mode 100644 README create mode 100644 run_server.py create mode 100644 web/css/leaflet.label.css create mode 100644 web/js/leaflet.label.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/README b/README new file mode 100644 index 0000000..e0430f2 --- /dev/null +++ b/README @@ -0,0 +1,29 @@ +Eventmap +======== + +This package is intended to allow keeping track of equipment +deployed at an event. Right now it's fairly basic, e.g. it doesn't +support multiple types of equipment. + +Usage +===== + +1. Setting up layers + +It is assumed that you are on a terrain that might have multiple +floors. You can input floorplans as pdf or png into the layers +directory. Let your layer be called floor_1.pdf you can optionally +supply meta information for that layer by adding a file floor_1.pdf.txt. +There you can e.g. provide a name for that layer or describe how it +should be scaled translated which allows you e.g. to align multiple floorplans. + +2. Generating layer information and tilesets + +Run the read_layers.py script. It will look for layers in the layers directory, +render tilesets for those layers and generate a json document providing information +about the available layers. + +3. Run the server + +Run the run_map.py script. It will run a webservice with a map where you +can enter/edit/view deployed equipment. diff --git a/run_server.py b/run_server.py new file mode 100644 index 0000000..8c284d2 --- /dev/null +++ b/run_server.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# +# This file serves the static files in the +# web directory and provides API endpoints +# to read/write the existing markers. +# + +import os +from cStringIO import StringIO +import hashlib +import threading +import json + +import cherrypy + +class SynchronizedJSON(object): + def __init__(self, filename): + self._filename = filename + if os.path.exists(self._filename): + with open(self._filename, 'rb') as f: + self._data = f.read() + else: + self._data = '{}' + + self._sync_id = '' + self._update_sync_id() + + self.lock = threading.Lock() + self.cond = threading.Condition(self.lock) + + def _update_sync_id(self): + doc = json.loads(self._data) + if 'sync-id' in doc: + del doc['sync-id'] + hashed_data = json.dumps(doc) + h = hashlib.sha256() + h.update(hashed_data) + doc['sync-id'] = h.hexdigest() + self._data = json.dumps(doc) + self._sync_id = h.hexdigest() + + @property + def data(self): + assert self.lock.locked() + return self._data + + @property + def sync_id(self): + assert self.lock.locked() + return self._sync_id + + def set_data(self, data): + assert self.lock.locked() + + if self._data == data: + return + + self._data = data + self._update_sync_id() + + with open(self._filename + '.new', 'wb') as f: + f.write(data) + f.flush() + os.fsync(f.fileno()) + os.rename(self._filename + '.new', self._filename) + + self.cond.notify_all() + +class EventMapMarkerApi(object): + def __init__(self, path): + self.marker_doc = SynchronizedJSON(os.path.join(path, 'markers.json')) + + @cherrypy.expose + def get(self): + cherrypy.response.headers['Content-Type']= 'application/json' + with self.marker_doc.lock: + return self.marker_doc.data + + @cherrypy.expose + def poll(self, current): + cherrypy.response.headers['Content-Type']= 'application/json' + with self.marker_doc.lock: + while self.marker_doc.sync_id == current: + self.marker_doc.cond.wait(2) + if cherrypy.engine.state != cherrypy.engine.states.STARTED: + break + yield ' ' + yield self.marker_doc.data + + @cherrypy.expose + def post(self): + content_length = cherrypy.request.headers['Content-Length'] + data = cherrypy.request.body.read(int(content_length)) + + doc = json.loads(data) + + # Check sync-id + with self.marker_doc.lock: + self.marker_doc.set_data(data) + +class EventMapApi(object): + def __init__(self, path): + self.markers = EventMapMarkerApi(path) + +def test_log(msg, level): + print "%s, %s" % (msg, level) + +if __name__ == '__main__': + current_dir = os.path.dirname(os.path.abspath(__file__)) + cherrypy.engine.subscribe('log', test_log) + cherrypy.config.update({ + 'server.socket_host': '127.0.0.1', + 'server.socket_port': 8023, + 'server.thread_pool_max': 500, + 'server.thread_pool': 100, + 'log.screen': True + }) + + cherrypy.tree.mount(None, '/', { + '/': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join(current_dir, 'web'), + 'tools.staticdir.index': 'index.html', + } + }) + + cherrypy.tree.mount(EventMapApi(current_dir), '/api', { + '/': { + 'response.timeout': 600, + 'response.stream': True + } + }) + + cherrypy.engine.signals.subscribe() + cherrypy.engine.start() + cherrypy.engine.block() diff --git a/web/css/leaflet.label.css b/web/css/leaflet.label.css new file mode 100644 index 0000000..8671515 --- /dev/null +++ b/web/css/leaflet.label.css @@ -0,0 +1,52 @@ +.leaflet-label { + background: rgb(235, 235, 235); + background: rgba(235, 235, 235, 0.81); + background-clip: padding-box; + border-color: #777; + border-color: rgba(0,0,0,0.25); + border-radius: 4px; + border-style: solid; + border-width: 4px; + color: #111; + display: block; + font: 12px/20px "Helvetica Neue", Arial, Helvetica, sans-serif; + font-weight: bold; + padding: 1px 6px; + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + white-space: nowrap; + z-index: 6; +} + +.leaflet-label.leaflet-clickable { + cursor: pointer; +} + +.leaflet-label:before, +.leaflet-label:after { + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + content: none; + position: absolute; + top: 5px; +} + +.leaflet-label:before { + border-right: 6px solid black; + border-right-color: inherit; + left: -10px; +} + +.leaflet-label:after { + border-left: 6px solid black; + border-left-color: inherit; + right: -10px; +} + +.leaflet-label-right:before, +.leaflet-label-left:after { + content: ""; +} \ No newline at end of file diff --git a/web/js/eventmap.js b/web/js/eventmap.js index 775bb79..290c03e 100644 --- a/web/js/eventmap.js +++ b/web/js/eventmap.js @@ -2,19 +2,168 @@ var map; var draw_control; var layers = {}; var recorded_obj; +var marker_store = {}; +var marker_store_sync_id; + +function eventmap_send_update() { + var update_doc = { + 'sync-id': marker_store_sync_id, + 'markers': {} + }; + + $.each(marker_store, function(marker_name, marker) { + update_doc.markers[marker_name] = {}; + + var marker_info = update_doc.markers[marker_name]; + + marker_info.lat = marker.getLatLng().lat; + marker_info.lng = marker.getLatLng().lng; + marker_info.layer = marker.options.layer_name; + }); + + $.ajax({ + url: 'api/markers/post', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(update_doc), + processData: false, + dataType: 'json' + }); +} + +function eventmap_process_update(data) { + if (typeof data == "string") + data = JSON.parse(data); + marker_store_sync_id = data['sync-id']; + $.each(data['markers'], function(marker_name, marker_info) { + if (marker_name in marker_store) { + var marker = marker_store[marker_name]; + var marker_pos = marker.getLatLng(); + + if (marker_pos.lat != marker_info.lat + || marker_pos.lng != marker_info.lng) + marker.setLatLng([marker_info.lat, marker_info.lng]); + + if (marker.options.layer_name != marker_info.layer) { + var old_lg = layers[marker.options.layer_name]; + var new_lg = layers[marker_info.layer]; + + var old_draw = old_lg.getLayers()[1]; + var new_draw = new_lg.getLayers()[1]; + + old_draw.removeLayer(marker); + new_draw.addLayer(marker); + marker.options.layer_name = marker_info.layer; + } + marker.options.sync_id = marker_store_sync_id; + console.log("Kept marker '" + marker_name + "'."); + } else { + var marker = L.marker([marker_info.lat, marker_info.lng]); + + marker.bindLabel('', { + noHide: marker_labels_no_hide + }); + + add_contextmenu(marker); + + marker.options.label_text = marker_name; + marker.updateLabelContent(marker.options.label_text); + marker_store[marker_name] = marker; + + marker.options.layer_name = marker_info.layer + layers[marker_info.layer].getLayers()[1].addLayer(marker); + marker.options.sync_id = marker_store_sync_id; + console.log("Added marker '" + marker_name + "'."); + } + }); + + for (var marker_name in marker_store) { + if (marker_store[marker_name].options.sync_id == + marker_store_sync_id) + continue; + + var marker = marker_store[marker_name]; + layers[marker.options.layer_name].getLayers()[1].removeLayer(marker); + delete marker_store[marker_name]; + console.log("Removed marker '" + marker_name + "'."); + } + + (function longpoll() { + $.ajax({ + url: 'api/markers/poll/' + marker_store_sync_id, + timeout: 600000 + }).done(eventmap_process_update).fail(function() { + setTimeout(longpoll, 10000); + }); + })(); +} + +function add_contextmenu(marker) { + marker.options.contextmenu = true; + marker.options.contextmenuItems = [ + { + text: 'Move', + callback: function() { + move_marker(marker); + } + }, + { + text: 'Rename', + callback: function() { + rename_marker(marker); + } + } + ]; + $.each(layers, function(layer_name, layer_object) { + marker.options.contextmenuItems.push({ + text: 'Send to ' + layer_name, + callback: function() { + $.each(layers, function(key, value) { + map.removeLayer(value); + drawing_layer = value.getLayers()[1]; + if (drawing_layer.hasLayer(marker)) + drawing_layer.removeLayer(marker); + }); + map.addLayer(layer_object); + layer_object.getLayers()[1].addLayer(marker); + marker.options.layer_name = layer_name; + eventmap_send_update(); + } + }) + }); + marker._initContextMenu(); +} /* Functionality of (re)naming a marker - if I understood how objects worked * in javascript, this should probably be one. :/ */ function rename_marker(marker) { var label_text; + var new_label_text; + if (marker.options.label_text === undefined) label_text = ''; else label_text = marker.options.label_text; - marker.options.label_text = prompt("Please enter name", label_text); + do { + new_label_text = prompt("Please enter name", label_text); + if (new_label_text in marker_store + && marker_store[new_label_text] !== marker) { + alert("This name is not unique!"); + } else { + break; + } + } while (1); + + if (marker.options.label_text !== undefined) + delete marker_store[label_text] + + marker.options.label_text = new_label_text; marker.updateLabelContent(marker.options.label_text); + + marker_store[new_label_text] = marker + eventmap_send_update(); } /* Functionality of moving a marker - if I understood how objects worked @@ -43,7 +192,7 @@ function move_marker_disable_events() { function move_marker_commit(e) { move_marker_disable_events(); - /* notify about editing */ + eventmap_send_update(); } function move_marker_keyup(e) { @@ -102,43 +251,14 @@ $(function() { if (!map.hasLayer(layer_object)) return true; - created_object.options.contextmenu = true; - created_object.options.contextmenuItems = [ - { - text: 'Move', - callback: function() { - move_marker(created_object); - } - }, - { - text: 'Rename', - callback: function() { - rename_marker(created_object); - } - } - ]; - $.each(layers, function(layer_name, layer_object) { - created_object.options.contextmenuItems.push({ - text: 'Send to ' + layer_name, - callback: function() { - $.each(layers, function(key, value) { - map.removeLayer(value); - drawing_layer = value.getLayers()[1]; - if (drawing_layer.hasLayer(created_object)) - drawing_layer.removeLayer(created_object); - }); - map.addLayer(layer_object); - layer_object.getLayers()[1].addLayer(created_object); - /* possibly notify about move */ - } - }) - }); - created_object._initContextMenu(); + add_contextmenu(created_object); layer_object.getLayers()[1].addLayer(created_object); + created_object.options.layer_name = layer_name; return false; }); rename_marker(created_object); + /* update will be sent by "rename_marker" */ }); $.getJSON('js/layers.json', function(data) { @@ -169,5 +289,11 @@ $(function() { } }); L.control.layers(layers, {}).addTo(map); + + $.ajax({ + url: 'api/markers/get' + }).done(eventmap_process_update).fail(function() { + alert("Couldn't load marker info from server!"); + }); }); }); diff --git a/web/js/leaflet.label.js b/web/js/leaflet.label.js new file mode 100644 index 0000000..736475c --- /dev/null +++ b/web/js/leaflet.label.js @@ -0,0 +1,9 @@ +/* + Leaflet.label, a plugin that adds labels to markers and vectors for Leaflet powered maps. + (c) 2012-2013, Jacob Toye, Smartrak + + https://github.com/Leaflet/Leaflet.label + http://leafletjs.com + https://github.com/jacobtoye +*/ +(function(){L.labelVersion="0.2.1-dev",L.Label=L.Class.extend({includes:L.Mixin.Events,options:{className:"",clickable:!1,direction:"right",noHide:!1,offset:[12,-15],opacity:1,zoomAnimation:!0},initialize:function(t,e){L.setOptions(this,t),this._source=e,this._animated=L.Browser.any3d&&this.options.zoomAnimation,this._isOpen=!1},onAdd:function(t){this._map=t,this._pane=this._source instanceof L.Marker?t._panes.markerPane:t._panes.popupPane,this._container||this._initLayout(),this._pane.appendChild(this._container),this._initInteraction(),this._update(),this.setOpacity(this.options.opacity),t.on("moveend",this._onMoveEnd,this).on("viewreset",this._onViewReset,this),this._animated&&t.on("zoomanim",this._zoomAnimation,this),L.Browser.touch&&!this.options.noHide&&L.DomEvent.on(this._container,"click",this.close,this)},onRemove:function(t){this._pane.removeChild(this._container),t.off({zoomanim:this._zoomAnimation,moveend:this._onMoveEnd,viewreset:this._onViewReset},this),this._removeInteraction(),this._map=null},setLatLng:function(t){return this._latlng=L.latLng(t),this._map&&this._updatePosition(),this},setContent:function(t){return this._previousContent=this._content,this._content=t,this._updateContent(),this},close:function(){var t=this._map;t&&(L.Browser.touch&&!this.options.noHide&&L.DomEvent.off(this._container,"click",this.close),t.removeLayer(this))},updateZIndex:function(t){this._zIndex=t,this._container&&this._zIndex&&(this._container.style.zIndex=t)},setOpacity:function(t){this.options.opacity=t,this._container&&L.DomUtil.setOpacity(this._container,t)},_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-label "+this.options.className+" leaflet-zoom-animated"),this.updateZIndex(this._zIndex)},_update:function(){this._map&&(this._container.style.visibility="hidden",this._updateContent(),this._updatePosition(),this._container.style.visibility="")},_updateContent:function(){this._content&&this._map&&this._prevContent!==this._content&&"string"==typeof this._content&&(this._container.innerHTML=this._content,this._prevContent=this._content,this._labelWidth=this._container.offsetWidth)},_updatePosition:function(){var t=this._map.latLngToLayerPoint(this._latlng);this._setPosition(t)},_setPosition:function(t){var e=this._map,i=this._container,n=e.latLngToContainerPoint(e.getCenter()),o=e.layerPointToContainerPoint(t),s=this.options.direction,a=this._labelWidth,l=L.point(this.options.offset);"right"===s||"auto"===s&&o.xi;i++)L.DomEvent.on(t,e[i],this._fireMouseEvent,this)}},_removeInteraction:function(){if(this.options.clickable){var t=this._container,e=["dblclick","mousedown","mouseover","mouseout","contextmenu"];L.DomUtil.removeClass(t,"leaflet-clickable"),L.DomEvent.off(t,"click",this._onMouseClick,this);for(var i=0;e.length>i;i++)L.DomEvent.off(t,e[i],this._fireMouseEvent,this)}},_onMouseClick:function(t){this.hasEventListeners(t.type)&&L.DomEvent.stopPropagation(t),this.fire(t.type,{originalEvent:t})},_fireMouseEvent:function(t){this.fire(t.type,{originalEvent:t}),"contextmenu"===t.type&&this.hasEventListeners(t.type)&&L.DomEvent.preventDefault(t),"mousedown"!==t.type?L.DomEvent.stopPropagation(t):L.DomEvent.preventDefault(t)}}),L.BaseMarkerMethods={showLabel:function(){return this.label&&this._map&&(this.label.setLatLng(this._latlng),this._map.showLabel(this.label)),this},hideLabel:function(){return this.label&&this.label.close(),this},setLabelNoHide:function(t){this._labelNoHide!==t&&(this._labelNoHide=t,t?(this._removeLabelRevealHandlers(),this.showLabel()):(this._addLabelRevealHandlers(),this.hideLabel()))},bindLabel:function(t,e){var i=this.options.icon?this.options.icon.options.labelAnchor:this.options.labelAnchor,n=L.point(i)||L.point(0,0);return n=n.add(L.Label.prototype.options.offset),e&&e.offset&&(n=n.add(e.offset)),e=L.Util.extend({offset:n},e),this._labelNoHide=e.noHide,this.label||(this._labelNoHide||this._addLabelRevealHandlers(),this.on("remove",this.hideLabel,this).on("move",this._moveLabel,this).on("add",this._onMarkerAdd,this),this._hasLabelHandlers=!0),this.label=new L.Label(e,this).setContent(t),this},unbindLabel:function(){return this.label&&(this.hideLabel(),this.label=null,this._hasLabelHandlers&&(this._labelNoHide||this._removeLabelRevealHandlers(),this.off("remove",this.hideLabel,this).off("move",this._moveLabel,this).off("add",this._onMarkerAdd,this)),this._hasLabelHandlers=!1),this},updateLabelContent:function(t){this.label&&this.label.setContent(t)},getLabel:function(){return this.label},_onMarkerAdd:function(){this._labelNoHide&&this.showLabel()},_addLabelRevealHandlers:function(){this.on("mouseover",this.showLabel,this).on("mouseout",this.hideLabel,this),L.Browser.touch&&this.on("click",this.showLabel,this)},_removeLabelRevealHandlers:function(){this.off("mouseover",this.showLabel,this).off("mouseout",this.hideLabel,this),L.Browser.touch&&this.off("click",this.showLabel,this)},_moveLabel:function(t){this.label.setLatLng(t.latlng)}},L.Icon.Default.mergeOptions({labelAnchor:new L.Point(9,-20)}),L.Marker.mergeOptions({icon:new L.Icon.Default}),L.Marker.include(L.BaseMarkerMethods),L.Marker.include({_originalUpdateZIndex:L.Marker.prototype._updateZIndex,_updateZIndex:function(t){var e=this._zIndex+t;this._originalUpdateZIndex(t),this.label&&this.label.updateZIndex(e)},_originalSetOpacity:L.Marker.prototype.setOpacity,setOpacity:function(t,e){this.options.labelHasSemiTransparency=e,this._originalSetOpacity(t)},_originalUpdateOpacity:L.Marker.prototype._updateOpacity,_updateOpacity:function(){var t=0===this.options.opacity?0:1;this._originalUpdateOpacity(),this.label&&this.label.setOpacity(this.options.labelHasSemiTransparency?this.options.opacity:t)},_originalSetLatLng:L.Marker.prototype.setLatLng,setLatLng:function(t){return this.label&&!this._labelNoHide&&this.hideLabel(),this._originalSetLatLng(t)}}),L.CircleMarker.mergeOptions({labelAnchor:new L.Point(0,0)}),L.CircleMarker.include(L.BaseMarkerMethods),L.Path.include({bindLabel:function(t,e){return this.label&&this.label.options===e||(this.label=new L.Label(e,this)),this.label.setContent(t),this._showLabelAdded||(this.on("mouseover",this._showLabel,this).on("mousemove",this._moveLabel,this).on("mouseout remove",this._hideLabel,this),L.Browser.touch&&this.on("click",this._showLabel,this),this._showLabelAdded=!0),this},unbindLabel:function(){return this.label&&(this._hideLabel(),this.label=null,this._showLabelAdded=!1,this.off("mouseover",this._showLabel,this).off("mousemove",this._moveLabel,this).off("mouseout remove",this._hideLabel,this)),this},updateLabelContent:function(t){this.label&&this.label.setContent(t)},_showLabel:function(t){this.label.setLatLng(t.latlng),this._map.showLabel(this.label)},_moveLabel:function(t){this.label.setLatLng(t.latlng)},_hideLabel:function(){this.label.close()}}),L.Map.include({showLabel:function(t){return this.addLayer(t)}}),L.FeatureGroup.include({clearLayers:function(){return this.unbindLabel(),this.eachLayer(this.removeLayer,this),this},bindLabel:function(t,e){return this.invoke("bindLabel",t,e)},unbindLabel:function(){return this.invoke("unbindLabel")},updateLabelContent:function(t){this.invoke("updateLabelContent",t)}})})(this,document); \ No newline at end of file -- cgit v1.2.1