diff --git a/app.py b/app.py index 2c6e924480f87590aeadf667e9c29f13b7f8ed2a..fbd5ea2e024fdee62604e5fdddc798eb4d3a4a3e 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,9 @@ 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 diff --git a/static/css/map.css b/static/css/map.css index 7adb1d8adfb92a0e1866385abd88bc405d65d812..fe9ecbb2a3ec5d9bd5540f22d45d049350fe9090 100644 --- a/static/css/map.css +++ b/static/css/map.css @@ -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; + } +} diff --git a/static/js/embed.js b/static/js/embed.js new file mode 100644 index 0000000000000000000000000000000000000000..4d0e85d31df79fac075f1d2c3e67f97eadea4888 --- /dev/null +++ b/static/js/embed.js @@ -0,0 +1,20 @@ +/* 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(); diff --git a/static/js/map.js b/static/js/map.js index 9f3489a89835b73c13085dd722a3e29ba9741f22..979a20a44015a86c9ef46561089d5e87c31b593e 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -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(" | "); + $('<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">×</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() { diff --git a/templates/map.html b/templates/map.html index f3c95ee8e43fff02c4db5abc8a9700cf44240bd3..74778eb23535f40cbd11475caab5779abad9be82 100644 --- a/templates/map.html +++ b/templates/map.html @@ -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);">×</a></div> - {% include "about.html" %} - </div> - <div class="dialog" id="welcome"> - <div class="close" id="close-welcome"><a href="javascript:void(0);">×</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 %} diff --git a/templates/about.html b/templates/modal-about.html similarity index 87% rename from templates/about.html rename to templates/modal-about.html index add748a655774d142fab1ecb4418ef9b0aa3e22d..e840bdda59c39ad9cf25bbad1d9ebbb2430c3ef6 100644 --- a/templates/about.html +++ b/templates/modal-about.html @@ -26,6 +26,6 @@ NextBus™ 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> diff --git a/templates/modal-embed.html b/templates/modal-embed.html new file mode 100644 index 0000000000000000000000000000000000000000..d0a3e3b61f57604cd851366f2bb50cae0da0f84a --- /dev/null +++ b/templates/modal-embed.html @@ -0,0 +1,30 @@ +<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> diff --git a/templates/welcome.html b/templates/modal-welcome.html similarity index 88% rename from templates/welcome.html rename to templates/modal-welcome.html index 4740d157ce30019a9836bb25a85552f4e4db0126..44da60a6f609990c9214bc12676ec730874b3b53 100644 --- a/templates/welcome.html +++ b/templates/modal-welcome.html @@ -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>