summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Franke <nobody@nowhere.ws>2013-12-12 13:15:52 +0100
committerChristian Franke <nobody@nowhere.ws>2013-12-12 13:15:52 +0100
commit0339962f3cbdc07ccadc6accb5482ffa416115f7 (patch)
treee493bbcb5afad65ad5a561d6a5af88a80274e36c
parent63f064e481b42252c0916359e752c678156c11e2 (diff)
Somewhat working, cherrypy's worker threads can't keep up, though :/
-rw-r--r--.gitignore1
-rw-r--r--README29
-rw-r--r--run_server.py136
-rw-r--r--web/css/leaflet.label.css52
-rw-r--r--web/js/eventmap.js194
-rw-r--r--web/js/leaflet.label.js9
6 files changed, 387 insertions, 34 deletions
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.x<n.x?(L.DomUtil.addClass(i,"leaflet-label-right"),L.DomUtil.removeClass(i,"leaflet-label-left"),t=t.add(l)):(L.DomUtil.addClass(i,"leaflet-label-left"),L.DomUtil.removeClass(i,"leaflet-label-right"),t=t.add(L.point(-l.x-a,l.y))),L.DomUtil.setPosition(i,t)},_zoomAnimation:function(t){var e=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center).round();this._setPosition(e)},_onMoveEnd:function(){this._animated&&"auto"!==this.options.direction||this._updatePosition()},_onViewReset:function(t){t&&t.hard&&this._update()},_initInteraction:function(){if(this.options.clickable){var t=this._container,e=["dblclick","mousedown","mouseover","mouseout","contextmenu"];L.DomUtil.addClass(t,"leaflet-clickable"),L.DomEvent.on(t,"click",this._onMouseClick,this);for(var i=0;e.length>i;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