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

Refactor/cleanup modals; Embed code generation

parent 381a7d2d
1 merge request!1Implemented Embed-code generator interface
import os
from flask import Flask, jsonify, render_template, request
from flask import Flask, jsonify, render_template, request, abort
from flask.ext.bower import Bower
from jinja2.exceptions import TemplateNotFound
from sqlalchemy.orm import joinedload
from models import db
from models import db, Agency
from datetime import datetime
app = Flask(__name__, instance_relative_config=True)
......@@ -22,15 +23,13 @@ Bower(app)
# Flask Web Routes
@app.route('/')
def map():
from models import Agency
# TODO: serve different agency depending on cookie (or special domain)
agency_tag = app.config['AGENCIES'][0]
agency = db.session.query(Agency).filter(Agency.tag==agency_tag).one()
return render_template('map.html', agency=agency, config=app.config)
@app.route('/embed')
@app.route('/e')
def map_embed():
from models import Agency
# TODO: serve different agency depending on cookie (or special domain)
agency_tag = app.config['AGENCIES'][0]
agency = db.session.query(Agency).filter(Agency.tag==agency_tag).one()
......@@ -38,14 +37,24 @@ def map_embed():
@app.route('/ajax')
def ajax():
# TODO: OPTIMIZE THIS SHIT.
# Over 1sec/request just to get predictions? Fuck that noise.
dataset = request.args.get('dataset')
agency = request.args.get('agency')
""" Handle all async requests (from JS). """
query = request.args.get('query')
agency_tag = app.config['AGENCIES'][0]
agency = db.session.query(Agency).filter(Agency.tag==agency_tag).one()
def modal(name):
""" Serve contents of a modal popup """
if not name.isalnum():
return abort(404)
template_name = 'modal-' + name + '.html'
try:
return render_template(template_name, agency=agency, config=app.config)
except TemplateNotFound:
return abort(404)
def routes():
from models import Agency, Route, RouteStop, Stop
""" Serve the route configuration (routes, stops) """
from models import Route, RouteStop, Stop
routes = db.session.query(Route).join(Agency)\
.filter(Agency.tag==agency).all()
.filter(Agency.tag==agency_tag).all()
stops = db.session.query(Stop).options(joinedload(Stop.routes))\
.filter(Stop.routes.any(Route.id.in_([r.id for r in routes]))).all()
return {
......@@ -54,8 +63,12 @@ def ajax():
}
def vehicles():
from models import Agency, Route, VehicleLocation, Prediction
""" Serve the current vehicle locations and arrival predictions. """
from models import Route, VehicleLocation, Prediction
# TODO: OPTIMIZE
# Over 1sec/request just to get predictions? Fuck that noise.
# TODO: Somehow bundle these queries into the object model definitions? So messy :(
# maybe VehicleLocation.get_latest_for_agency(a)?
# 1. Select the latest vehicle locations for each vehicle. (The DB may have old ones too).
v_inner = db.session.query(VehicleLocation.vehicle,
db.func.max(VehicleLocation.time).label("time"))\
......@@ -63,7 +76,7 @@ def ajax():
vehicle_locations = db.session.query(VehicleLocation).join(v_inner, db.and_(
v_inner.c.vehicle == VehicleLocation.vehicle,
v_inner.c.time == VehicleLocation.time
)).filter(Agency.tag==agency).all()
)).filter(Agency.tag==agency_tag).all()
# 2. Select the predictions for each vehicle:stop pair which came from the most recent
# API call for that vehicle:stop pair. Old predictions may be stored but we don't want them.
now = datetime.now()
......@@ -76,22 +89,22 @@ def ajax():
p_inner.c.vehicle == Prediction.vehicle,
p_inner.c.stop_id == Prediction.stop_id
)).filter(
Agency.tag==agency,
Agency.tag==agency_tag,
Prediction.prediction >= now)\
.group_by(Prediction.id, Prediction.vehicle, Prediction.stop_id)\
.all()
z = {
return {
"locations": {v.vehicle: v.serialize() for v in vehicle_locations},
"predictions": {p.id: p.serialize() for p in predictions}
}
return z
if dataset == "routes":
r = jsonify(routes())
elif dataset == "vehicles":
r = jsonify(vehicles())
return r
if query == "routes":
return jsonify(routes())
elif query == "vehicles":
return jsonify(vehicles())
elif query == "modal":
modal_name = request.args.get('modal_name')
return modal(modal_name)
if __name__ == '__main__':
# Run Flask
......
......@@ -6,6 +6,7 @@ html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 14px;
}
#map {
height: 100%;
......@@ -26,7 +27,7 @@ html, body {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 5000;
z-index: 1001;
}
#locate a {
text-decoration: none;
......@@ -40,7 +41,7 @@ html, body {
}
/* Embed Mode - Hide UI stuff */
#map.embed .leaflet-control,
#map.embed .leaflet-control-zoom,
#map.embed ~ #locate {
display: none;
}
......@@ -77,7 +78,9 @@ html, body {
border-left: 1px solid #eee;
border-top-left-radius: 3px;
color: #eee;
z-index: 5000;
}
#map .leaflet-control-attribution a {
cursor: pointer;
}
#map .leaflet-control-zoom,
#map .leaflet-control-zoom a {
......@@ -131,7 +134,7 @@ html, body {
font-size: 1.3em;
color: #fff;
}
.dialog {
.modal {
display: none;
background-color: #444;
color: #eee;
......@@ -145,29 +148,31 @@ html, body {
padding: 1em;
max-height: 80%;
overflow: auto;
z-index: 2000;
}
.dialog .close {
.modal .close {
float: right;
cursor: pointer;
}
.dialog h1,
.dialog h2 {
.modal h1,
.modal h2 {
text-align: center;
}
.dialog a,
.dialog a:hover,
.dialog a:visited,
.dialog a:active {
.modal a,
.modal a:hover,
.modal a:visited,
.modal a:active {
color: #fff;
font-weight: bold;
text-decoration: none;
}
.dialog h1 a,
.dialog h2 a {
.modal h1 a,
.modal h2 a {
text-decoration: underline;
}
@media all and (max-width: 600px) {
.dialog {
.modal {
width: 100%;
left: 0;
top: 10%;
......@@ -176,3 +181,14 @@ html, body {
border-right: none;
}
}
@media all and (max-height: 400px) {
.modal {
max-height: 100%;
height: 100%;
top: 0;
border-radius: 0;
border-top: none;
border-bottom: none;
}
}
/* Logic for embed code generator form */
var updateCode = function() {
var code = $("#embed-preview").html();
$("#embed-code").html(code);
}
$("#embed-height").change(function() {
$("#embed-preview iframe").attr('height', $(this).val() + "px");
updateCode();
});
$("#embed-width").change(function() {
$("#embed-preview iframe").attr('width', $(this).val() + "px");
updateCode();
});
$("#embed-code").on('focus click keydown keyup', function() {
$(this).select();
});
updateCode();
......@@ -63,6 +63,7 @@ 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);
......@@ -70,13 +71,44 @@ BusMap.Map = function(opts) {
if (that.opts.refresh.vehicles) {
setInterval(updateVehicles, that.opts.refresh.vehicles * 1000);
}
// Remove leaflet default attribution (replaced later)
$('.leaflet-control-attribution').html("");
if (!that.opts.embed) {
// UI stuff for regular mode
$('<a>About</a>').click(function() { showModal('about') })
.appendTo('.leaflet-control-attribution');
$('.leaflet-control-attribution').append("&nbsp;|&nbsp;");
$('<a>Embed</a>').click(function() { showModal('embed') })
.appendTo('.leaflet-control-attribution');
// Display welcome screen on first launch
var been_here = BusMap.getCookie('been_here');
if (!(been_here)) {
$("#welcome").show();
}
BusMap.setCookie('been_here', Date.now());
// Find the user
that.leaflet.on('locationfound', function(l) {
that.leaflet.fitBounds(l.bounds);
});
$("#locate a").click(function() {
that.leaflet.locate({
timeout: 3000,
});
});
} else {
// UI stuff for embed mode
$('<a>?</a>').click(function() { showModal('about') })
.appendTo('.leaflet-control-attribution');
}
};
/* Get Routes (and Stops, and Directions) */
function updateRoutes() {
var url = "ajax";
var params = {
dataset: "routes",
query: "routes",
agency: that.opts.agency,
};
$.getJSON(url, params)
......@@ -92,7 +124,7 @@ BusMap.Map = function(opts) {
function updateVehicles() {
var url = "ajax";
var params = {
dataset: "vehicles",
query: "vehicles",
agency: that.opts.agency,
};
$.getJSON(url, params)
......@@ -320,7 +352,6 @@ BusMap.Map = function(opts) {
var marker = that.stopMarkers[view.substring(1)];
var ll = marker.getLatLng();
that.leaflet.setView(ll, that.stopZoom);
marker.openPopup();
}
function _avsVehicle(view) {
if (!that.vehicleMarkers) {
......@@ -329,7 +360,6 @@ BusMap.Map = function(opts) {
var marker = that.vehicleMarkers[view.substring(1)];
var ll = marker.getLatLng();
that.leaflet.setView(ll, that.vehicleZoom);
marker.openPopup();
}
function _avsLatLonZoom(view) {
view = view.split(",");
......@@ -349,23 +379,48 @@ BusMap.Map = function(opts) {
BusMap.setCookie('last_view', view);
}
// Display a modal pop-up
function showModal(name) {
$('div.modal').remove();
var modal = $('<div class="modal"></div>');
var closeBtn = $('<div class="close">&times;</div>').appendTo(modal);
closeBtn.click(function() { $(this).parent().remove(); });
$("#map").after(modal);
modal.show();
var params = {
agency: that.opts.agency,
query: "modal",
modal_name: name,
};
$.get("ajax", params).done(function(contents) {
modal.append(contents);
});
}
// 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);
$('#msg-zoomForVehicles').hide();
if (!that.opts.embed) $('#msg-zoomForVehicles').hide();
} else if (zoom < that.zoomShowVehicles && that.leaflet.hasLayer(that.vehicleMarkersGroup)) {
that.leaflet.removeLayer(that.vehicleMarkersGroup);
$('#msg-zoomForVehicles').show();
if (!that.opts.embed) $('#msg-zoomForVehicles').show();
}
}
}
that.setViewFromUrlHash = function() {
var hash = window.location.hash.substring(1);
return applyViewString(hash);
if (hash == "") return false;
var parts = hash.split(";");
// First part: view string (lat,lon,zoom; vehicle; or stop)
applyViewString(parts[0]);
// Optional second part: name of modal to open (about, welcome, ...)
if (parts.length > 1) {
showModal(parts[1]);
}
}
that.getUrlFromView = function() {
......
......@@ -12,14 +12,7 @@
{% endblock %}
{% block body %}
<div id="map" class="{% if embed %}embed{% endif %}"></div>
<div class="dialog" id="about">
<div class="close" id="close-about"><a href="javascript:void(0);">&times;</a></div>
{% include "about.html" %}
</div>
<div class="dialog" id="welcome">
<div class="close" id="close-welcome"><a href="javascript:void(0);">&times;</a></div>
{% include "welcome.html" %}
</div>
{# TODO: Generate these using JS, not here. #}
<div id="msg">
<span id="msg-zoomForVehicles">Zoom in to see vehicles.</span>
</div>
......@@ -55,30 +48,5 @@
vehicles: 5,
},
});
{% if not embed %}
// 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(); });
$(".dialog .close").click(function() { $(this).parent().hide(); });
// Display welcome screen on first launch
var been_here = BusMap.getCookie('been_here');
if (!(been_here)) {
$("#welcome").show();
}
BusMap.setCookie('been_here', Date.now());
// Find the user
map.leaflet.on('locationfound', function(l) {
map.leaflet.fitBounds(l.bounds);
});
$("#locate a").click(function() {
map.leaflet.locate({
timeout: 3000,
});
});
{% endif %}
</script>
{% endblock %}
......@@ -26,6 +26,6 @@
NextBus&trade; is a trademark of NextBus Inc.
</p>
<p class="legalese">
Not guaranteed to be correct.
Refer to your travel agency for official schedules and/or predictions.
Data not guaranteed to be correct.
Refer to your transit agency for official schedules and/or predictions.
</p>
<h2>Embed This Map</h2>
<p>
Use PyBusMap in your own website or display!
</p>
<p>
Tweak the options to your liking, then copy the code.
</p>
<form id="form">
<label for="embed-height">Height</label>
<input id="embed-height" type="number" min=50 step=50 value=250>
<br>
<label for="embed-width">Width</label>
<input id="embed-width" type="number" min=50 step=50 value=400>
<br>
<label for="embed-mode">Mode</label>
<select id="embed-mode">
<option value="map">Map</option>
<option value="predictions">Predictions</option>
<option value="split">Combined</option>
</select>
</form>
<div id="embed-preview">
<iframe src="{{ url_for('map_embed', _external=True, _scheme='') }}" width="400px" height="250px" frameborder=0></iframe>
</div>
<textarea id="embed-code" cols=100 rows=5 readonly=true></textarea>
<p class="legalese">
Data not guaranteed to be correct.
Refer to your transit agency for official schedules and/or predictions.
</p>
<script src="static/js/embed.js"></script>
......@@ -14,5 +14,5 @@
</h1>
<p class="legalese">
Not guaranteed to be correct.
Refer to your travel agency for official schedules and/or predictions.
Refer to your transit agency for official schedules and/or predictions.
</p>
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