Added maps page
This commit is contained in:
parent
464ff1ff57
commit
e216eb4792
5 changed files with 327 additions and 9 deletions
|
@ -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/<path>')
|
||||
@app.route('/static/<path>', 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:
|
||||
|
||||
|
|
9
platypush/backend/http/static/css/map.css
Normal file
9
platypush/backend/http/static/css/map.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: static !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
197
platypush/backend/http/static/js/map.js
Normal file
197
platypush/backend/http/static/js/map.js
Normal file
|
@ -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();
|
||||
}
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
<head>
|
||||
<title>Platypush Web Console</title>
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton-tabs.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}">
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/skeleton-tabs.js') }}"></script>
|
||||
|
|
39
platypush/backend/http/templates/map.html
Normal file
39
platypush/backend/http/templates/map.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<head>
|
||||
<title>Platypush map service</title>
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/skeleton.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/map.css') }}">
|
||||
<link href='//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700' rel='stylesheet' type='text/css'>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/map.js') }}"></script>
|
||||
|
||||
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ api_key }}&v=3.exp&callback=initMap"></script>
|
||||
<script type="text/javascript">
|
||||
window.db_conf = JSON.parse('{{ utils.to_json(config["db"]) | safe }}');
|
||||
window.websocket_port = {{ websocket_port }};
|
||||
window.map_start = "{{ start }}";
|
||||
window.map_end = "{{ end }}";
|
||||
|
||||
{% if token %}
|
||||
window.token = '{{ token }}'
|
||||
{% else %}
|
||||
window.token = undefined
|
||||
{% endif %}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<div id="map-container" width="100" height="100">
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</main>
|
||||
<body>
|
||||
|
Loading…
Reference in a new issue