diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index 000c9e8a..ed118933 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -1,4 +1,6 @@ import asyncio +import datetime +import dateutil.parser import inspect import json import os @@ -30,7 +32,8 @@ class HttpBackend(Backend): } def __init__(self, port=8008, websocket_port=8009, disable_websocket=False, - redis_queue='platypush_flask_mq', token=None, dashboard={}, **kwargs): + redis_queue='platypush_flask_mq', token=None, dashboard={}, + maps={}, **kwargs): super().__init__(**kwargs) self.port = port @@ -38,6 +41,7 @@ class HttpBackend(Backend): self.redis_queue = redis_queue self.token = token self.dashboard = dashboard + self.maps = maps self.server_proc = None self.disable_websocket = disable_websocket self.websocket_thread = None @@ -131,15 +135,76 @@ class HttpBackend(Backend): self.redis.rpush(self.redis_queue, event) return jsonify({ 'status': 'ok' }) - @app.route('/static/') + @app.route('/static/', methods=['GET']) def static_path(path): return send_from_directory(static_dir, filename) - @app.route('/dashboard') + @app.route('/dashboard', methods=['GET']) def dashboard(): return render_template('dashboard.html', config=self.dashboard, utils=HttpUtils, token=self.token, websocket_port=self.websocket_port) + @app.route('/map', methods=['GET']) + def map(): + """ + Supported values for `start` and `end`: + - now + - yesterday + - -30s (it means '30 seconds ago') + - -10m (it means '10 minutes ago') + - -24h (it means '24 hours ago') + - -7d (it means '7 days ago') + - 2018-06-04T17:39:22.742Z (ISO strings) + + Default: start=yesterday, end=now + """ + def parse_time(time_string): + if not time_string: + return None + + now = datetime.datetime.now() + + if time_string == 'now': + return now.isoformat() + if time_string == 'yesterday': + return (now - datetime.timedelta(days=1)).isoformat() + + try: + return dateutil.parser.parse(time_string).isoformat() + except ValueError: + pass + + m = re.match('([-+]?)(\d+)([dhms])', time_string) + if not m: + raise RuntimeError('Invalid time interval string representation: "{}"'. + format(time_string)) + + time_delta = (-1 if m.group(1) == '-' else 1) * int(m.group(2)) + time_unit = m.group(3) + + if time_unit == 'd': + params = { 'days': time_delta } + elif time_unit == 'h': + params = { 'hours': time_delta } + elif time_unit == 'm': + params = { 'minutes': time_delta } + elif time_unit == 's': + params = { 'seconds': time_delta } + + return (now + datetime.timedelta(**params)).isoformat() + + try: + api_key = self.maps['api_key'] + except KeyError: + raise RuntimeError('Google Maps api_key not set in the maps configuration') + + start = parse_time(http_request.args.get('start', default='yesterday')) + end = parse_time(http_request.args.get('end', default='now')) + return render_template('map.html', config=self.maps, + utils=HttpUtils, start=start, end=end, + token=self.token, api_key=api_key, + websocket_port=self.websocket_port) + return app @@ -240,6 +305,14 @@ class HttpUtils(object): results = cls.search_directory(os.path.join(basedir, directory), *extensions) return [item[len(basedir):] for item in results] + @classmethod + def to_json(cls, data): + return json.dumps(data) + + @classmethod + def from_json(cls, data): + return json.loads(data) + # vim:sw=4:ts=4:et: diff --git a/platypush/backend/http/static/css/map.css b/platypush/backend/http/static/css/map.css new file mode 100644 index 00000000..b23160e8 --- /dev/null +++ b/platypush/backend/http/static/css/map.css @@ -0,0 +1,9 @@ +#map { + width: 100%; + height: 100%; + position: static !important; + margin: 0; + padding: 0; + overflow: hidden; +} + diff --git a/platypush/backend/http/static/js/map.js b/platypush/backend/http/static/js/map.js new file mode 100644 index 00000000..0c40998a --- /dev/null +++ b/platypush/backend/http/static/js/map.js @@ -0,0 +1,197 @@ +function initMapFromGeopoints(points) { + var $mapContainer = $('#map-container'), + $map = $('#map'); + + var markerArray = []; + var directionsService = new google.maps.DirectionsService; + + var map = new google.maps.Map($map.get(0), { + mapTypeId: google.maps.MapTypeId.ROADMAP, + }); + + var bounds = new google.maps.LatLngBounds(); + var infowindow = new google.maps.InfoWindow(); + var maxPoints = 22; + + var minDist = 100; + var start = points.length > maxPoints ? points.length-maxPoints-1 : 0; + var lastPoint; + + for (i = points.length-1; i >= 0 && markerArray.length <= maxPoints; i--) { + if (lastPoint && latLngDistance( + [points[i]['latitude'], points[i]['longitude']], + [lastPoint['latitude'], lastPoint['longitude']]) < minDist) { + continue; + } + + lastPoint = points[i]; + var marker = new google.maps.Marker({ + position: new google.maps.LatLng(points[i]['latitude'], points[i]['longitude']), + map: map + }); + + google.maps.event.addListener(marker, 'click', (function(marker, i) { + return function() { + infowindow.setContent( + (points[i]['address'] || '[No address]') + ' @ ' + + Date.parse(points[i]['created_at']).toLocaleString()); + infowindow.open(map, marker); + } + })(marker, i)); + + // Extend the bounds to include each marker's position + bounds.extend(marker.position); + markerArray.push(marker); + } + + var listener = google.maps.event.addListener(map, 'idle', function () { + // Now fit the map to the newly inclusive bounds + map.fitBounds(bounds); + setTimeout(function() { + map.setZoom(getBoundsZoomLevel(bounds, $map.children().width(), $map.children().height())); + }, 1000); + + google.maps.event.removeListener(listener); + }); + + // Create a renderer for directions and bind it to the map. + var directionsDisplay = new google.maps.DirectionsRenderer({map: map}); + + // Instantiate an info window to hold step text. + var stepDisplay = new google.maps.InfoWindow; + + // Display the route between the initial start and end selections. + calculateAndDisplayRoute( + directionsDisplay, directionsService, markerArray, stepDisplay, map); +} + +function calculateAndDisplayRoute(directionsDisplay, directionsService, + markerArray, stepDisplay, map) { + // First, remove any existing markers from the map. + for (var i = 0; i < markerArray.length; i++) { + markerArray[i].setMap(null); + } + + var waypoints = []; + for (i=1; i < markerArray.length-1; i++) { + if (!waypoints) waypoints = []; + waypoints.push({ + location: markerArray[i].getPosition(), + stopover: true, + }); + } + + // Retrieve the start and end locations and create a DirectionsRequest using + // WALKING directions. + directionsService.route({ + origin: markerArray[0].getPosition(), + destination: markerArray[markerArray.length-1].getPosition(), + waypoints: waypoints, + travelMode: 'WALKING' + }, function(response, status) { + // Route the directions and pass the response to a function to create + // markers for each step. + if (status === 'OK') { + directionsDisplay.setDirections(response); + showSteps(response, markerArray, stepDisplay, map); + } else { + // window.alert('Directions request failed due to ' + status); + } + }); +} + +function showSteps(directionResult, markerArray, stepDisplay, map) { + // For each step, place a marker, and add the text to the marker's infowindow. + // Also attach the marker to an array so we can keep track of it and remove it + // when calculating new routes. + var myRoute = directionResult.routes[0].legs[0]; + for (var i = 0; i < myRoute.steps.length; i++) { + var marker = markerArray[i] = markerArray[i] || new google.maps.Marker; + marker.setMap(map); + marker.setPosition(myRoute.steps[i].start_location); + } +} + +function getBoundsZoomLevel(bounds, width, height) { + var WORLD_DIM = { height: 256, width: 256 }; + var ZOOM_MAX = 21; + + function latRad(lat) { + var sin = Math.sin(lat * Math.PI / 180); + var radX2 = Math.log((1 + sin) / (1 - sin)) / 2; + return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2; + } + + function zoom(mapPx, worldPx, fraction) { + return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2); + } + + var ne = bounds.getNorthEast(); + var sw = bounds.getSouthWest(); + + var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI; + + var lngDiff = ne.lng() - sw.lng(); + var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360; + + var latZoom = zoom(height, WORLD_DIM.height, latFraction); + var lngZoom = zoom(width, WORLD_DIM.width, lngFraction); + + return Math.min(latZoom, lngZoom, ZOOM_MAX); +} + +function latLngDistance(a, b) { + if (typeof(Number.prototype.toRad) === "undefined") { + Number.prototype.toRad = function() { + return this * Math.PI / 180; + } + } + + var R = 6371e3; // metres + var phi1 = a[0].toRad(); + var phi2 = b[0].toRad(); + var delta_phi = (b[0]-a[0]).toRad(); + var delta_lambda = (b[1]-a[1]).toRad(); + + var a = Math.sin(delta_phi/2) * Math.sin(delta_phi/2) + + Math.cos(phi1) * Math.cos(phi2) * + Math.sin(delta_lambda/2) * Math.sin(delta_lambda/2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return R * c; +} + +function updateGeoPoints() { + var from = new Date(window.map_start).toISOString(); + var to = new Date(window.map_end).toISOString(); + from = from.substring(0, 10) + ' ' + from.substring(11, 19) + to = to.substring(0, 10) + ' ' + to.substring(11, 19) + + var engine = window.db_conf.engine; + var table = window.db_conf.table; + var columns = window.db_conf.columns; + + execute( + { + type: 'request', + action: 'db.select', + args: { + engine: engine, + query: "SELECT " + columns['latitude'] + " AS latitude, " + + columns['longitude'] + " AS longitude, " + + columns['address'] + " AS address, " + + "DATE_FORMAT(" + columns['created_at'] + ", '%Y-%m-%dT%TZ') " + + "AS created_at FROM " + table + " WHERE created_at BETWEEN '" + + from + "' AND '" + to + "' ORDER BY " + columns['created_at'] + " DESC" } + }, + + onSuccess = function(response) { + initMapFromGeopoints(response.response.output); + } + ); +} + +function initMap() { + updateGeoPoints(); +} + diff --git a/platypush/backend/http/templates/index.html b/platypush/backend/http/templates/index.html index fa5f2490..2fd88486 100644 --- a/platypush/backend/http/templates/index.html +++ b/platypush/backend/http/templates/index.html @@ -2,12 +2,12 @@ Platypush Web Console - - - - - - + + + + + + diff --git a/platypush/backend/http/templates/map.html b/platypush/backend/http/templates/map.html new file mode 100644 index 00000000..6f6cab5f --- /dev/null +++ b/platypush/backend/http/templates/map.html @@ -0,0 +1,39 @@ + + + Platypush map service + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ +