Beginning of JavaScript-only routing UI
authorRichard Fairhurst <richard@systemeD.net>
Tue, 21 Jan 2014 13:29:05 +0000 (13:29 +0000)
committerRichard Fairhurst <richard@systemeD.net>
Tue, 21 Jan 2014 13:29:05 +0000 (13:29 +0000)
app/assets/javascripts/index.js
app/assets/javascripts/routing.js.erb [new file with mode: 0644]
app/assets/stylesheets/common.css.scss
app/views/layouts/_search.html.erb
vendor/assets/leaflet/leaflet.polyline.js [new file with mode: 0755]

index 55e612f17668f6e3da055b2fdcf003b3a03c9285..4645a3e2e23a482934c9e19b7a9f03fc6b582748 100644 (file)
@@ -5,6 +5,7 @@
 //= require leaflet.key
 //= require leaflet.note
 //= require leaflet.share
+//= require leaflet.polyline
 //= require index/search
 //= require index/browse
 //= require index/export
@@ -13,6 +14,7 @@
 //= require index/note
 //= require index/new_note
 //= require router
+//= require routing
 
 (function() {
   var loaderTimeout;
@@ -322,12 +324,18 @@ $(document).ready(function () {
 
   $(".search_form").on("submit", function(e) {
     e.preventDefault();
-    $("header").addClass("closed");
-    var query = $(this).find("input[name=query]").val();
-    if (query) {
-      OSM.router.route("/search?query=" + encodeURIComponent(query) + OSM.formatHash(map));
+    if ($(".query_wrapper.routing").is(":visible")) {
+      // Routing
+      OSM.routing.requestRoute();
     } else {
-      OSM.router.route("/" + OSM.formatHash(map));
+      // Search
+      $("header").addClass("closed");
+      var query = $(this).find("input[name=query]").val();
+      if (query) {
+        OSM.router.route("/search?query=" + encodeURIComponent(query) + OSM.formatHash(map));
+      } else {
+        OSM.router.route("/" + OSM.formatHash(map));
+      }
     }
   });
 
@@ -338,4 +346,19 @@ $(document).ready(function () {
       map.getCenter().lat.toFixed(precision) + "," +
       map.getCenter().lng.toFixed(precision)));
   });
+
+  $(".get_directions").on("click",function(e) {
+       e.preventDefault();
+       $(".query_wrapper.search").hide();
+       $(".query_wrapper.routing").show();
+  });
+
+  $(".close_directions").on("click",function(e) {
+       e.preventDefault();
+       $(".query_wrapper.search").show();
+       $(".query_wrapper.routing").hide();
+  });
+
+  OSM.routing = OSM.Routing(map,'OSM.routing',$('.query_wrapper.routing'));
+
 });
diff --git a/app/assets/javascripts/routing.js.erb b/app/assets/javascripts/routing.js.erb
new file mode 100644 (file)
index 0000000..3084647
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+       osm.org routing interface
+       
+       See also:
+       https://github.com/apmon/openstreetmap-website/tree/routing2
+       https://github.com/apmon/openstreetmap-website/compare/routing2
+       https://github.com/apmon/openstreetmap-website/blob/9755c3ae0a8d0684d43760f91dc864ff42d8477a/app/views/routing/start.js.erb
+
+       *** draggable start/end markers
+       
+*/
+
+var TURN_INSTRUCTIONS=["",
+       "Continue on ",                         // 1
+       "Slight right onto ",           // 2
+       "Turn right onto ",                     // 3
+       "Sharp right onto ",            // 4
+       "U-turn along ",                        // 5
+       "Sharp left onto ",                     // 6
+       "Turn left onto ",                      // 7
+       "Slight left onto ",            // 8
+       "(via point) ",                         // 9
+       "Follow ",                                      // 10
+       "At roundabout take ",          // 11
+       "Leave roundabout - ",          // 12
+       "Stay on roundabout - ",        // 13
+       "Start at end of ",                     // 14
+       "Reach destination",            // 15
+       "Go against one-way on ",       // 16
+       "End of one-way on "]           // 17
+
+var ROUTING_POLYLINE={
+       color: '#03f',
+       opacity: 0.3,
+       weight: 10
+};
+
+
+OSM.Routing=function(map,name,jqSearch) {
+       var r={};
+       r.map=map;                              // Leaflet map
+       r.name=name;                    // global variable name of this instance (needed for JSONP)
+       r.jqSearch=jqSearch;    // JQuery object for search panel
+
+       r.route_from=null;
+       r.route_to=null;
+       r.viaPoints=[];
+       r.polyline=null;
+
+       // Geocoding
+
+       r.geocode=function(id,event) { var _this=this;
+               var field=event.target;
+               var v=event.target.value;
+               // *** do something if v==''
+               var querystring = '<%= NOMINATIM_URL %>search?q=' + encodeURIComponent(v) + '&format=json';
+               // *** &accept-language=<%#= request.user_preferred_languages.join(',') %>
+               // *** prefer current viewport
+               $.getJSON(querystring, function(json) { _this._gotGeocode(json,field); });
+       };
+       
+       r._gotGeocode=function(json,field) {
+               if (json.length==0) {
+                       alert("Sorry, couldn't find that place.");      // *** internationalise
+                       r[field.id]=null;
+                       return;
+               }
+               field.value=json[0].display_name;
+               var lat=Number(json[0].lat), lon=Number(json[0].lon);
+               r[field.id]=[lat,lon];
+               // ** update markers
+       };
+       
+       // Route-fetching UI
+
+       r.requestRoute=function() {
+               if (r.route_from && r.route_to) {
+                       var chosen=jqSearch.find('select.routing_engines :selected').val();
+                       r.engines[chosen].getRoute(true,[r.route_from,r.route_to]);
+                       // then, when the route has been fetched, it'll call the engine's gotRoute function
+               }
+       };
+
+       // Take an array of Leaflet LatLngs and draw it as a polyline
+       r.setPolyline=function(line) {
+               if (r.polyline) map.removeLayer(r.polyline);
+               r.polyline=L.polyline(line, ROUTING_POLYLINE).addTo(r.map);
+               // *** zoom to fit
+       };
+
+       // Take an array of directions and write it out
+       // (we use OSRM's route_instructions format)
+       r.setItinerary=function(steps) {
+               $("#content").removeClass("overlay-sidebar");
+               $('#sidebar_content').empty();
+               var html="";
+               for (var i=0; i<steps.length; i++) {
+                       var s=steps[i];
+                       html+="<div class='route_step'>";
+                       html+=TURN_INSTRUCTIONS[s[0]] || s[0];
+                       html+=s[1];
+                       html+="</div>";
+               }
+        $('#sidebar_content').html(html);
+       };
+
+
+       // Add engines
+       
+       r.engines=[];
+       r.addEngine=function(engine) {
+               // Save engine
+               var i=r.engines.length;
+               engine.subscript=i;
+               r['engine'+i]=engine;
+               r.engines.push(engine);
+
+               // Add generic JSONP function
+               engine.requestJSONP=function(url) {
+                       var script = document.createElement('script');
+                       script.src = url+"&jsonp="+r.name+".engine"+this.subscript+".gotRoute";
+                       // OSRM doesn't like non-alphanumeric, otherwise we could just do OSM.routing.engines["+engine.subscript+"].gotRoute
+                       document.body.appendChild(script); 
+               };
+
+               // Populate dropdown
+               var select=jqSearch.find('select.routing_engines');
+               select.append("<option value='"+i+"'>"+engine.name+"</option>");
+       };
+
+       // OSRM car engine
+       // *** this should all be shared from an OSRM library somewhere
+       // *** need to clear hints at some point
+
+       r.addEngine({
+               name: 'Car (OSRM)',
+               draggable: true,
+               _hints: {},
+               getRoute: function(final,points) {
+                       var url="http://router.project-osrm.org/viaroute?z=14&output=json";
+                       for (var i=0; i<points.length; i++) {
+                               var pair=points[i].join(',');
+                               url+="&loc="+pair;
+                               if (this._hints[pair]) url+= "&hint="+this._hints[pair];
+                       }
+                       if (final) url+="&instructions=true";
+                       this.requestJSONP(url);
+               },
+               gotRoute: function(data) {
+                       // *** save hints
+                       var line=L.PolylineUtil.decode(data.route_geometry);
+                       for (i=0; i<line.length; i++) { line[i].lat/=10; line[i].lng/=10; }
+                       r.setPolyline(line);
+                       r.setItinerary(data.route_instructions);
+               }
+       });
+
+       return r;
+};
index 088d4e07a16325ee71224b44fcfa02f25c4d8a6a..a784493d675324b394f1c40f89e756efdae7550d 100644 (file)
@@ -953,6 +953,10 @@ header .search_form {
     font-size: 10px;
     color: $blue;
   }
+
+  .query_wrapper.routing {
+    display: none;
+  }
 }
 
 /* Rules for the map key which appears in the popout sidebar */
index 732e57d31505f20f6382c40c7fe238c6fe4e8ea1..acc910b48361a5206b7b9696088dcf524305191c 100644 (file)
@@ -1,7 +1,22 @@
 <%= form_tag search_path, :class => "search_form" do %>
   <%= submit_tag t('site.search.submit_text') %>
-  <div class='query_wrapper'>
+
+  <div class='query_wrapper search'>
     <%= text_field_tag "query", params[:query], :placeholder => t("site.search.search"), :autofocus => autofocus %>
-    <%= link_to t('site.search.where_am_i'), '#', { :class => "describe_location", :title => t('site.search.where_am_i_title') } %>
+    <div class='query_options'>
+      <%= link_to t('site.search.get_directions'), '#', { :class => "get_directions", :title => t('site.search.get_directions_title') } %>
+      &middot;
+      <%= link_to t('site.search.where_am_i'), '#', { :class => "describe_location", :title => t('site.search.where_am_i_title') } %>
+    </div>
   </div>
+
+  <div class='query_wrapper routing'>
+    <%= text_field_tag "route_from", params[:from], :placeholder => "From", :onchange=>"OSM.routing.geocode('route_from',event)" %>
+    <%= text_field_tag "route_to"  , params[:to]  , :placeholder => "To"  , :onchange=>"OSM.routing.geocode('route_to'  ,event)" %>
+    <div class='query_options'>
+      <select class='routing_engines'></select>
+      <%= link_to t('site.search.close_directions'), '#', { :class => "close_directions", :title => t('site.search.close_directions_title') } %>
+    </div>
+  </div>
+
 <% end %>
diff --git a/vendor/assets/leaflet/leaflet.polyline.js b/vendor/assets/leaflet/leaflet.polyline.js
new file mode 100755 (executable)
index 0000000..b7e85d6
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * L.PolylineUtil contains utilify functions for polylines, two methods
+ * are added to the L.Polyline object to support creation of polylines
+ * from an encoded string and converting existing polylines to an
+ * encoded string.
+ *
+ *  - L.Polyline.fromEncoded(encoded [, options]) returns a L.Polyline
+ *  - L.Polyline.encodePath() returns a string
+ *
+ * Actual code from:
+ * http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/\
+ */
+
+/*jshint browser:true, debug: true, strict:false, globalstrict:false, indent:4, white:true, smarttabs:true*/
+/*global L:true, console:true*/
+
+
+// Inject functionality into Leaflet
+(function (L) {
+       if (!(L.Polyline.prototype.fromEncoded)) {
+               L.Polyline.fromEncoded = function (encoded, options) {
+                       return new L.Polyline(L.PolylineUtil.decode(encoded), options);
+               };
+       }
+       if (!(L.Polygon.prototype.fromEncoded)) {
+               L.Polygon.fromEncoded = function (encoded, options) {
+                       return new L.Polygon(L.PolylineUtil.decode(encoded), options);
+               };
+       }
+
+       var encodeMixin = {
+               encodePath: function () {
+                       return L.PolylineUtil.encode(this.getLatLngs());
+               }
+       };
+
+       if (!L.Polyline.prototype.encodePath) {
+               L.Polyline.include(encodeMixin);
+       }
+       if (!L.Polygon.prototype.encodePath) {
+               L.Polygon.include(encodeMixin);
+       }
+})(L);
+
+// Utility functions.
+L.PolylineUtil = {};
+
+L.PolylineUtil.encode = function (latlngs) {
+       var i, dlat, dlng;
+       var plat = 0;
+       var plng = 0;
+       var encoded_points = "";
+
+       for (i = 0; i < latlngs.length; i++) {
+               var lat = latlngs[i].lat;
+               var lng = latlngs[i].lng;
+               var late5 = Math.floor(lat * 1e5);
+               var lnge5 = Math.floor(lng * 1e5);
+               dlat = late5 - plat;
+               dlng = lnge5 - plng;
+               plat = late5;
+               plng = lnge5;
+               encoded_points +=
+                       L.PolylineUtil.encodeSignedNumber(dlat) +
+                       L.PolylineUtil.encodeSignedNumber(dlng);
+       }
+       return encoded_points;
+};
+
+// This function is very similar to Google's, but I added
+// some stuff to deal with the double slash issue.
+L.PolylineUtil.encodeNumber = function (num) {
+       var encodeString = "";
+       var nextValue, finalValue;
+       while (num >= 0x20) {
+               nextValue = (0x20 | (num & 0x1f)) + 63;
+               encodeString += (String.fromCharCode(nextValue));
+               num >>= 5;
+       }
+       finalValue = num + 63;
+       encodeString += (String.fromCharCode(finalValue));
+       return encodeString;
+};
+
+// This one is Google's verbatim.
+L.PolylineUtil.encodeSignedNumber = function (num) {
+       var sgn_num = num << 1;
+       if (num < 0) {
+               sgn_num = ~(sgn_num);
+       }
+       return (L.PolylineUtil.encodeNumber(sgn_num));
+};
+
+L.PolylineUtil.decode = function (encoded) {
+       var len = encoded.length;
+       var index = 0;
+       var latlngs = [];
+       var lat = 0;
+       var lng = 0;
+
+       while (index < len) {
+               var b;
+               var shift = 0;
+               var result = 0;
+               do {
+                       b = encoded.charCodeAt(index++) - 63;
+                       result |= (b & 0x1f) << shift;
+                       shift += 5;
+               } while (b >= 0x20);
+               var dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
+               lat += dlat;
+
+               shift = 0;
+               result = 0;
+               do {
+                       b = encoded.charCodeAt(index++) - 63;
+                       result |= (b & 0x1f) << shift;
+                       shift += 5;
+               } while (b >= 0x20);
+               var dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
+               lng += dlng;
+
+               latlngs.push(new L.LatLng(lat * 1e-5, lng * 1e-5));
+       }
+
+       return latlngs;
+};
\ No newline at end of file