Skip to content
Snippets Groups Projects
Commit 10268c46 authored by Anton Sarukhanov's avatar Anton Sarukhanov
Browse files

Built vehicles into the UI. Also the attached prediction bubbles. Added dep for vehiclerotate lib.

parent afd1497a
No related branches found
No related tags found
No related merge requests found
......@@ -15,6 +15,9 @@
],
"dependencies": {
"leaflet": "~0.7.7",
"jquery": "~2.1.4"
"jquery": "~2.1.4",
"leaflet.markercluster": "~0.4.0",
"leaflet-marker-rotate": "https://git.xhost.io/anton/leaflet-marker-rotate.git",
"Leaflet.label": "~0.2.1"
}
}
......@@ -17,9 +17,9 @@ class Config(object):
'task': 'celerytasks.update_predictions',
'schedule': timedelta(seconds=9),
},
'update-vehicle-locations-every-4s': {
'update-vehicle-locations-every-3s': {
'task': 'celerytasks.update_vehicle_locations',
'schedule': timedelta(seconds=4),
'schedule': timedelta(seconds=3),
},
'delete-stale-predictions-every-5m': {
'task': 'celerytasks.delete_stale_predictions',
......@@ -35,21 +35,18 @@ class Config(object):
LOCATIONS_MAX_AGE = 5 * 60;
AGENCIES = ['rutgers']
# Distance within stops with the same tag will be averaged to one lat/lon point.
# Guide: http://gis.stackexchange.com/a/8674
# Stops with the same tag within this distance of each other will be averaged to one lat/lon point.
# 0.001 = 110 Meters (football field)
SAME_STOP_LAT = 0.005
SAME_STOP_LON = 0.005
# Map display parameters
MAP_CUSTOM_ATTRIBUTION = '<a href="https://ant.sr/">ant.sr</a>'
MAP_DATA_ATTRIBUTION = '<a href="http://cartodb.com/attributions#basemaps">CartoDB</a>'
MAP_ERROR_TILE_URL = 'http://tiles.antsar-static.com/generic/tile-blank-black.png'
MAP_TILE_URL = 'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'
MAP_TILE_SUBDOMAINS = ['a', 'b', 'c']
MAP_TILESET = 'rutgers-black'
MAP_LAT_PADDING = 0.01
MAP_LON_PADDING = 0.01
MAP_LAT_PADDING = 0.03
MAP_LON_PADDING = 0.03
class ProdConfig(Config):
SQLALCHEMY_URI = 'postgresql://localhost/pybusmap_prod'
......
......@@ -20,6 +20,18 @@ html, body {
color: #ff0;
}
/* Messages displayed above the map */
#msg {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: #f00;
}
#msg span {
display: none;
}
/* Attribution link / "About" popup */
#map .leaflet-control-attribution {
background-color: rgba(0,0,0,.8);
......@@ -36,9 +48,27 @@ html, body {
font-weight: bold;
text-decoration: none;
}
#map div.leaflet-popup a.leaflet-popup-close-button {
#map div.leaflet-popup header,
#map div.leaflet-popup footer {
text-align: center;
}
#map div.leaflet-popup footer {
opacity: .75;
}
#map div.leaflet-popup .predictions .none {
opacity: .75;
}
#map div.leaflet-popup .predictions.vehicle-predictions .stop:nth-child(n+6) {
display: none;
}
#map div.leaflet-popup .show-all-predictions .predictions.vehicle-predictions .stop:nth-child(n+6) {
display: block;
}
#map div.leaflet-popup .show-all-predictions .predictions.vehicle-predictions .more {
display: none;
padding: 2px 2px 0 0;
}
#map div.leaflet-popup .predictions.vehicle-predictions .more {
text-align: center;
}
#map div.leaflet-popup div.leaflet-popup-content-wrapper {
border: 3px solid #666;
......@@ -84,6 +114,7 @@ html, body {
#about a:active {
color: #fff;
font-weight: bold;
text-decoration: none;
}
@media all and (max-width: 600px) {
......
static/img/bus.png

2.91 KiB

File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
/* The primary class for this project */
var BusMap = {
cookiePrefix: "BM_"
cookiePrefix: "BM_",
zoomShowVehicles: 15,
};
/*
......@@ -10,7 +11,6 @@ var BusMap = {
BusMap.Map = function(opts) {
this.opts = opts;
var stops = {};
var stopMarkers = {};
var routes = {};
var that = this;
init();
......@@ -32,19 +32,14 @@ BusMap.Map = function(opts) {
else {that.leaflet.fitBounds(that.opts.bounds)}
if (that.opts.zoom) that.leaflet.setZoom(that.opts.zoom);
// Put "About" link into the attribution box
$(".leaflet-control-attribution")
.html('<a id="show-about" href="#">About</a>');
$("#show-about").click(function() { $("#about").show(); });
$("#close-about").click(function() { $("#about").hide(); });
// Restore the user's last view (if exists).
goToLastView();
lastViewRecover();
// Store view parameters for recovery later.
that.leaflet.on('moveend', function() {
updateLastView();
});
that.leaflet.on('moveend', lastViewStore);
// Show/hide markers based on zoom.
that.leaflet.on('zoomend', zoomShowHide);
// Configure and apply the map tile layer
var tileUrl = that.opts.tileUrl;
......@@ -60,7 +55,6 @@ BusMap.Map = function(opts) {
// Fetch initial data
updateRoutes();
updateVehicles();
// Begin timed data updates
if (that.opts.refresh.routes) {
setInterval(updateRoutes, that.opts.refresh.routes * 1000);
......@@ -74,8 +68,8 @@ BusMap.Map = function(opts) {
function updateRoutes() {
var url = "ajax";
var params = {
"dataset": "routes",
"agency": that.opts.agency,
dataset: "routes",
agency: that.opts.agency,
};
$.getJSON(url, params)
.done(function(data) {
......@@ -90,8 +84,8 @@ BusMap.Map = function(opts) {
function updateVehicles() {
var url = "ajax";
var params = {
"dataset": "vehicles",
"agency": that.opts.agency,
dataset: "vehicles",
agency: that.opts.agency,
};
$.getJSON(url, params)
.done(function(data) {
......@@ -102,7 +96,7 @@ BusMap.Map = function(opts) {
that.stops[s].predictions = {};
}
for (var v in that.vehicles) {
that.vehicles[v].predictions = {};
that.vehicles[v].predictions = [];
}
for (var p in data.predictions) {
pr = data.predictions[p];
......@@ -115,7 +109,7 @@ BusMap.Map = function(opts) {
}
if (that.vehicles && pr.vehicle in that.vehicles) {
// Store this prediction with the relevant vehicle
that.vehicles[pr.vehicle].predictions[pr.stop_id] = pr;
that.vehicles[pr.vehicle].predictions.push(pr);
}
}
that.vehicles = data.locations;
......@@ -127,7 +121,87 @@ BusMap.Map = function(opts) {
/* Refresh (and/or create) UI elements for Vehicles */
function updateVehiclesUI(vehicles) {
return that;
if (!(that.vehicleMarkersGroup)) {
that.vehicleMarkersGroup = L.layerGroup();
that.leaflet.addLayer(that.vehicleMarkersGroup);
}
if (!that.vehicleMarkers) {
that.vehicleMarkers = {};
}
for (var v in vehicles) {
if (that.routes) {
if (vehicles[v].route && vehicles[v].route in that.routes) {
var route = that.routes[vehicles[v].route].title;
} else {
// This is debugging... Don't leave this shit here.
var route = "No Route ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
console.log("Vehicle \"" + v + "\" references unknown route \"" + vehicles[v].route + "\"");
}
} else {
var route = vehicles[v].vehicle;
}
var text = '<header>' + route + '</header>';
var text_after = '<footer>Bus # ' + vehicles[v].vehicle + '</footer>';
var popupOpts = {
closeButton: false,
keepInView: true,
};
if (!(v in that.vehicleMarkers)) {
var markerIcon = L.icon({
iconUrl: 'static/img/bus.png',
iconSize: [21,25],
iconAnchor: [10,12],
});
that.vehicleMarkers[v] = L.marker([vehicles[v].lat, vehicles[v].lon], {
icon: markerIcon,
iconAngle: vehicles[v].heading
}).bindLabel(route, {
noHide: true,
direction: 'right',
className: 'bus-label',
}).bindPopup(text + text_after, popupOpts).addTo(that.vehicleMarkersGroup);
} else {
that.vehicleMarkers[v].setLatLng([vehicles[v].lat, vehicles[v].lon])
.setIconAngle(vehicles[v].heading);
}
// Add predictions to the marker popup, if available
if (that.stops && vehicles[v].predictions) {
var predictions = [];
var now = new Date();
var offset_mins = now.getTimezoneOffset();
psorted = vehicles[v].predictions.sort(function(a,b){
return new Date(a.prediction).getTime() - new Date(b.prediction).getTime();
});
for (p in psorted) {
pr = psorted[p];
var p_line = "<strong>" + that.stops[pr.stop_id].title + "</strong>: ";
var pdate = new Date(pr.prediction);
var diff_sec = (pdate.getTime() - now.getTime()) / 1000;
var diff_min = Math.ceil(diff_sec / 60) + offset_mins;
// CSS classes for predictions
var pclass = "";
if (diff_min <= 1) { pclass = "lt1min"; }
else if (diff_min <= 2 ) { pclass = "lt2mins"; }
else if (diff_min <= 5 ) { pclass = "lt5mins"; }
p_line += ("<span class='prediction " + pclass
+ "' title='" + pr.vehicle + "'>"
+ diff_min + "</span>");
predictions.push("<div class='stop'" + p_line + "</div>");
}
if (predictions.length == 0) {
predictions = ['<div class="none">No arrival predictions.</div>'];
} else if (predictions.length > 5) {
predictions.push("<div class='more'><a href='javascript:void(0);' onclick='$(this).hide().parent().parent().parent().addClass(\"show-all-predictions\");'>see more</a></div>");
}
text += '<section class="predictions vehicle-predictions">'
+ predictions.join("") + '</section>' + text_after;
that.vehicleMarkers[v]._popup.setContent(text);
}
}
// Call this here to hide vehicles if we shouldn't be showing them just yet.
zoomShowHide();
}
/* Refresh (and/or create) UI elements for Stops */
......@@ -137,18 +211,20 @@ BusMap.Map = function(opts) {
disableClusteringAtZoom: 14,
maxClusterRadius: 40,
showCoverageOnHover: false,
zoomToBoundsOnClick: false,
});
that.leaflet.addLayer(that.stopMarkersClusterGroup);
}
if (!that.stopMarkers) {
that.stopMarkers = {};
}
for (var s in stops) {
var text = '<header>' + stops[s].title + '</header>';
var popupOpts = {
closeButton: true,
closeButton: false,
keepInView: true,
};
if (!(s in stopMarkers)) {
/* Stop marker doesn't exist yet - create it now. */
if (!(s in that.stopMarkers)) {
// Stop marker doesn't exist yet - create it now.
var markerIcon = L.icon({
iconUrl: 'static/img/stop27x60.png',
iconSize: [13, 30],
......@@ -159,17 +235,20 @@ BusMap.Map = function(opts) {
icon: markerIcon,
opacity: 1,
};
stopMarkers[s] = L.marker(
that.stopMarkers[s] = L.marker(
[stops[s].lat, stops[s].lon],
markerOpts).bindPopup(text, popupOpts);
that.stopMarkersClusterGroup.addLayer(stopMarkers[s]);
that.stopMarkersClusterGroup.addLayer(that.stopMarkers[s]);
}
/* Add predictions to the marker popup, if available */
if (that.stops[s].predictions) {
// Add predictions to the marker popup, if available
if (stops[s].predictions) {
var predictions = [];
var now = new Date();
var offset_mins = now.getTimezoneOffset();
for (r in stops[s].predictions) {
if (!(r in that.routes)) {
console.log("Unknown route " + r + " for stop " + stops[s].title);
}
var p_line = "<strong>" + that.routes[r].title + "</strong>: ";
var times = [];
// Sort by estimated time to arrival
......@@ -191,20 +270,20 @@ BusMap.Map = function(opts) {
+ diff_min + "</span>");
}
p_line += times.join(", ");
predictions.push(p_line);
predictions.push("<span class='route'" + p_line + "</span>");
}
if (predictions.length == 0) {
predictions = ['No vehicle arrival predictions.'];
predictions = ['<span class="none">No arrival predictions.</span>'];
}
text += '<section class="predictions">'
text += '<section class="predictions stop-predictions">'
+ predictions.sort().join("<br>") + '</section>';
stopMarkers[s]._popup.setContent(text);
that.stopMarkers[s]._popup.setContent(text);
}
}
return that;
}
function goToLastView() {
// Map view persistence functions
function lastViewRecover() {
var last = BusMap.getCookie('last_view');
if (last && last != "") {
last = last.split(",");
......@@ -214,8 +293,7 @@ BusMap.Map = function(opts) {
return false;
}
}
function updateLastView() {
function lastViewStore() {
var ll = that.leaflet.getCenter();
view = Math.round(ll.lat * 1000000) / 1000000 + ','
+ Math.round(ll.lng * 1000000) / 1000000 + ','
......@@ -223,6 +301,18 @@ BusMap.Map = function(opts) {
BusMap.setCookie('last_view', view);
}
// Scaling: update what is displayed based on zoom level
function zoomShowHide() {
var zoom = that.leaflet.getZoom();
if (that.vehicleMarkersGroup) {
if (zoom >= that.zoomShowVehicles && !(that.leaflet.hasLayer(that.vehicleMarkersGroup))) {
that.leaflet.addLayer(that.vehicleMarkersGroup);
} else if (zoom < that.zoomShowVehicles && that.leaflet.hasLayer(that.vehicleMarkersGroup)) {
that.leaflet.removeLayer(that.vehicleMarkersGroup);
}
}
}
return that;
};
......
......@@ -7,6 +7,7 @@
{{ super() }}
<link href="bower/leaflet/dist/leaflet.css" rel="stylesheet"/>
<link href="bower/leaflet.markercluster/dist/MarkerCluster.Default.css" rel="stylesheet"/>
<link href="bower/Leaflet.label/dist/leaflet.label.css" rel="stylesheet"/>
<link href="static/css/full-page-map.css" rel="stylesheet" />
{% endblock %}
{% block body %}
......@@ -38,11 +39,17 @@
Data sourced from the <a href="http://nextbus.com">Nextbus</a> API.
</p>
</div>
<div id="msg">
<span id="msg-zoomForVehicles">Zoom in to see vehicles</span>
</div>
<script src="bower/leaflet/dist/leaflet.js"></script>
<script src="bower/Leaflet.label/dist/leaflet.label.js"></script>
<script src="bower/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
<script src="bower/leaflet-marker-rotate/leaflet.marker.rotate.js"></script>
<script src="bower/jquery/dist/jquery.min.js"></script>
<script src="static/js/map.js"></script>
<script>
// Initialize the map
var map = BusMap.Map({
agency: {{ agency.tag|tojson|safe }},
mapElement: $("#map").get(0),
......@@ -63,5 +70,12 @@
vehicles: 5,
},
});
// Put "About" link into the attribution box
$(".leaflet-control-attribution")
.html('<a id="show-about" href="javascript:void(0)">About</a>');
$("#show-about").click(function() { $("#about").show(); });
$("#close-about").click(function() { $("#about").hide(); });
</script>
{% endblock %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment