From ad4f7c229fa30bbef93bb5df02b71ba235570ec3 Mon Sep 17 00:00:00 2001
From: Anton Sarukhanov <code@ant.sr>
Date: Sun, 10 Apr 2016 22:01:18 -0400
Subject: [PATCH] Refactor/cleanup modals; Embed code generation

---
 app.py                                        | 57 +++++++++------
 static/css/map.css                            | 44 ++++++++----
 static/js/embed.js                            | 20 ++++++
 static/js/map.js                              | 69 +++++++++++++++++--
 templates/map.html                            | 34 +--------
 templates/{about.html => modal-about.html}    |  4 +-
 templates/modal-embed.html                    | 30 ++++++++
 .../{welcome.html => modal-welcome.html}      |  2 +-
 8 files changed, 181 insertions(+), 79 deletions(-)
 create mode 100644 static/js/embed.js
 rename templates/{about.html => modal-about.html} (87%)
 create mode 100644 templates/modal-embed.html
 rename templates/{welcome.html => modal-welcome.html} (88%)

diff --git a/app.py b/app.py
index 2c6e924..fbd5ea2 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 7adb1d8..fe9ecbb 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 0000000..4d0e85d
--- /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 9f3489a..979a20a 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("&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() {
diff --git a/templates/map.html b/templates/map.html
index f3c95ee..74778eb 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);">&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 %}
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 add748a..e840bdd 100644
--- a/templates/about.html
+++ b/templates/modal-about.html
@@ -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>
diff --git a/templates/modal-embed.html b/templates/modal-embed.html
new file mode 100644
index 0000000..d0a3e3b
--- /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 4740d15..44da60a 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>
-- 
GitLab