]> git.openstreetmap.org Git - rails.git/commitdiff
Merge remote-tracking branch 'origin/master' into routing
authorTom Hughes <tom@compton.nu>
Sat, 8 Mar 2014 17:44:22 +0000 (17:44 +0000)
committerTom Hughes <tom@compton.nu>
Sat, 8 Mar 2014 17:44:22 +0000 (17:44 +0000)
Conflicts:
app/views/layouts/_search.html.erb

13 files changed:
app/assets/images/routing-sprite.png [new file with mode: 0644]
app/assets/images/searching-small.gif [new file with mode: 0644]
app/assets/javascripts/index.js
app/assets/javascripts/routing.js.erb [new file with mode: 0644]
app/assets/javascripts/routing_engines/graphhopper.js [new file with mode: 0644]
app/assets/javascripts/routing_engines/mapquest.js [new file with mode: 0644]
app/assets/javascripts/routing_engines/osrm.js [new file with mode: 0644]
app/assets/stylesheets/common.css.scss
app/views/layouts/_search.html.erb
config/environments/production.rb
config/locales/de.yml
config/locales/en.yml
vendor/assets/leaflet/leaflet.polyline.js [new file with mode: 0755]

diff --git a/app/assets/images/routing-sprite.png b/app/assets/images/routing-sprite.png
new file mode 100644 (file)
index 0000000..37d9488
Binary files /dev/null and b/app/assets/images/routing-sprite.png differ
diff --git a/app/assets/images/searching-small.gif b/app/assets/images/searching-small.gif
new file mode 100644 (file)
index 0000000..06dbc2b
Binary files /dev/null and b/app/assets/images/searching-small.gif differ
index dc3c932773066ce56a518a9b1c921659cf5ee7e8..e5f6fe47811a4f275f4814a0a7267dc5cee9b3f6 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,8 @@
 //= require index/note
 //= require index/new_note
 //= require router
+//= require routing
+//= require_tree ./routing_engines
 
 (function() {
   var loaderTimeout;
@@ -322,12 +325,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(true, true);
     } 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 +347,33 @@ $(document).ready(function () {
       map.getCenter().lat.toFixed(precision) + "," +
       map.getCenter().lng.toFixed(precision)));
   });
+
+  OSM.routing = OSM.Routing(map,'OSM.routing',$('.query_wrapper.routing'));
+  OSM.routing.chooseEngine('javascripts.directions.engines.osrm_car');
+
+  $(".get_directions").on("click",function(e) {
+    e.preventDefault();
+    $(".search").hide();
+    $(".routing").show();
+    $(".query_wrapper.routing [name=route_from]").focus();
+    $("#map").on('dragend dragover',function(e) { e.preventDefault(); });
+    $("#map").on('drop',function(e) { OSM.routing.handleDrop(e); e.preventDefault(); });
+    $(".routing_marker").on('dragstart',function(e) {
+      e.originalEvent.dataTransfer.effectAllowed = 'move';
+      e.originalEvent.dataTransfer.setData('id', this.id);
+      e.originalEvent.dataTransfer.setData('offsetX', e.originalEvent.target.width/2 - (e.originalEvent.x-e.target.x));
+      e.originalEvent.dataTransfer.setData('offsetY', e.originalEvent.target.height  - (e.originalEvent.y-e.target.y));
+    });
+  });
+
+  $(".close_directions").on("click",function(e) {
+    e.preventDefault();
+    $(".search").show();
+    $(".routing").hide();
+    OSM.routing.close();
+    $("#map").off('dragend drop dragover');
+    $(".routing_marker").off('dragstart');
+    $(".query_wrapper.search [name=query]").focus();
+  });
+
 });
diff --git a/app/assets/javascripts/routing.js.erb b/app/assets/javascripts/routing.js.erb
new file mode 100644 (file)
index 0000000..d62e94f
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+    osm.org routing interface
+*/
+
+var TURN_INSTRUCTIONS=[]
+
+var ROUTING_POLYLINE={
+    color: '#03f',
+    opacity: 0.3,
+    weight: 10
+};
+
+
+OSM.RoutingEngines={
+    list: []
+};
+
+OSM.Routing=function(map,name,jqSearch) {
+    var r={};
+
+    TURN_INSTRUCTIONS=["",
+    I18n.t('javascripts.directions.instructions.continue_on'),      // 1
+    I18n.t('javascripts.directions.instructions.slight_right'),     // 2
+    I18n.t('javascripts.directions.instructions.turn_right'),       // 3
+    I18n.t('javascripts.directions.instructions.sharp_right'),      // 4
+    I18n.t('javascripts.directions.instructions.uturn'),            // 5
+    I18n.t('javascripts.directions.instructions.sharp_left'),       // 6
+    I18n.t('javascripts.directions.instructions.turn_left'),        // 7
+    I18n.t('javascripts.directions.instructions.slight_left'),      // 8
+    I18n.t('javascripts.directions.instructions.via_point'),        // 9
+    I18n.t('javascripts.directions.instructions.follow'),           // 10
+    I18n.t('javascripts.directions.instructions.roundabout'),       // 11
+    I18n.t('javascripts.directions.instructions.leave_roundabout'), // 12
+    I18n.t('javascripts.directions.instructions.stay_roundabout'),  // 13
+    I18n.t('javascripts.directions.instructions.start'),            // 14
+    I18n.t('javascripts.directions.instructions.destination'),      // 15
+    I18n.t('javascripts.directions.instructions.against_oneway'),   // 16
+    I18n.t('javascripts.directions.instructions.end_oneway')]       // 17
+
+    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;      // null=unset, false=awaiting response, [lat,lon]=geocoded
+    r.route_to=null;        //  |
+    r.awaitingGeocode=false;// true if the user has requested a route, but we're waiting on a geocode result
+    r.awaitingRoute=false;  // true if we've asked the engine for a route and are waiting to hear back
+    r.dragging=false;       // true if the user is dragging a start/end point
+    r.viaPoints=[];         // not yet used
+
+    r.polyline=null;        // Leaflet polyline object
+    r.popup=null;           // Leaflet popup object
+    r.marker_from=null;     // Leaflet from marker
+    r.marker_to=null;       // Leaflet to marker
+
+    r.chosenEngine=null;    // currently selected routing engine
+
+    var icon_from = L.icon({
+        iconUrl: <%= asset_path('marker-green.png').to_json %>,
+        iconSize: [25, 41],
+        iconAnchor: [12, 41],
+        popupAnchor: [1, -34],
+        shadowUrl: <%= asset_path('images/marker-shadow.png').to_json %>,
+        shadowSize: [41, 41]
+    });
+    var icon_to = L.icon({
+        iconUrl: <%= asset_path('marker-red.png').to_json %>,
+        iconSize: [25, 41],
+        iconAnchor: [12, 41],
+        popupAnchor: [1, -34],
+        shadowUrl: <%= asset_path('images/marker-shadow.png').to_json %>,
+        shadowSize: [41, 41]
+    });
+
+    // Geocoding
+
+    r.geocode=function(id,event) { var _this=this;
+        var field=event.target;
+        var v=event.target.value;
+        var querystring = '<%= NOMINATIM_URL %>search?q=' + encodeURIComponent(v) + '&format=json';
+        // *** &accept-language=<%#= request.user_preferred_languages.join(',') %>
+        r[field.id]=false;
+        $.getJSON(querystring, function(json) { _this._gotGeocode(json,field); });
+    };
+    
+    r._gotGeocode=function(json,field) {
+        if (json.length==0) {
+            alert(I18n.t('javascripts.directions.errors.no_place'));
+            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];
+        r.updateMarker(field.id);
+        if (r.awaitingGeocode) {
+            r.awaitingGeocode=false;
+            r.requestRoute(true, true);
+        }
+    };
+
+    // Drag and drop markers
+    
+    r.handleDrop=function(e) {
+        var oe=e.originalEvent;
+        var id=oe.dataTransfer.getData('id');
+        var pt=L.DomEvent.getMousePosition(oe,map.getContainer());  // co-ordinates of the mouse pointer at present
+        pt.x+=Number(oe.dataTransfer.getData('offsetX'));
+        pt.y+=Number(oe.dataTransfer.getData('offsetY'));
+        var ll=map.containerPointToLatLng(pt);
+        r.createMarker(ll,id);
+        r.setNumericInput(ll,id);
+        r.requestRoute(true, false);
+        // update to/from field
+    };
+    r.createMarker=function(latlng,id) {
+        if (r[id]) r.map.removeLayer(r[id]);
+        r[id]=L.marker(latlng, {
+            icon: id=='marker_from' ? icon_from : icon_to,
+            draggable: true,
+            name: id
+        }).addTo(r.map);
+        r[id].on('drag',r.markerDragged);
+        r[id].on('dragend',r.markerDragged);
+    };
+    // Update marker from geocoded route input
+    r.updateMarker=function(id) {
+        var m=id.replace('route','marker');
+        if (!r[m]) { r.createMarker(r[id],m); return; }
+        var ll=r[m].getLatLng();
+        if (ll.lat!=r[id][0] || ll.lng!=r[id][1]) {
+            r.createMarker(r[id],m);
+        }
+    };
+    // Marker has been dragged
+    r.markerDragged=function(e) {
+        r.dragging=(e.type=='drag');    // true for drag, false for dragend
+        if (r.dragging && !r.chosenEngine.draggable) return;
+        if (r.dragging && r.awaitingRoute) return;
+        r.setNumericInput(e.target.getLatLng(), e.target.options.name);
+        r.requestRoute(!r.dragging, false);
+    };
+    // Set a route input field to a numeric value
+    r.setNumericInput=function(ll,id) {
+        var routeid=id.replace('marker','route');
+        r[routeid]=[ll.lat,ll.lng];
+        $("[name="+routeid+"]:visible").val(Math.round(ll.lat*10000)/10000+" "+Math.round(ll.lng*10000)/10000);
+    }
+    
+    // Route-fetching UI
+
+    r.requestRoute=function(isFinal, updateZoom) {
+        if (r.route_from && r.route_to) {
+            $(".query_wrapper.routing .spinner").show();
+            r.awaitingRoute=true;
+            r.chosenEngine.getRoute(isFinal,[r.route_from,r.route_to]);
+            if(updateZoom){
+                r.map.fitBounds(L.latLngBounds([r.route_from, r.route_to]).pad(0.05));
+            }
+            // then, when the route has been fetched, it'll call the engine's gotRoute function
+        } else if (r.route_from==false || r.route_to==false) {
+            // we're waiting for a Nominatim response before we can request a route
+            r.awaitingGeocode=true;
+        }
+    };
+
+    // 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);
+    };
+
+    // Take directions and write them out
+    // data = { steps: array of [latlng, sprite number, instruction text, distance in metres] }
+    // sprite numbers equate to OSRM's route_instructions turn values
+    r.setItinerary=function(data) {
+        // Create base table
+        $("#content").removeClass("overlay-sidebar");
+        $('#sidebar_content').empty();
+        var html=('<h2><a class="geolink" href="#" onclick="$(~.close_directions~).click();return false;">' +
+                  '<span class="icon close"></span></a>' + I18n.t('javascripts.directions.directions') + 
+                  '</h2><p id="routing_summary">' + 
+                  I18n.t('javascripts.directions.distance') + ': ' + r.formatDistance(data.distance)+ '. ' +
+                  I18n.t('javascripts.directions.time'    ) + ': ' + r.formatTime(data.time) + '.</p>' +
+                  '<table id="turnbyturn" />').replace(/~/g,"'");
+        $('#sidebar_content').html(html);
+        // Add each row
+        var cumulative=0;
+        for (var i=0; i<data.steps.length; i++) {
+            var step=data.steps[i];
+            // Distance
+            var dist=step[3];
+            if (dist<5) { dist=""; }
+            else if (dist<200) { dist=Math.round(dist/10)*10+"m"; }
+            else if (dist<1500) { dist=Math.round(dist/100)*100+"m"; }
+            else if (dist<5000) { dist=Math.round(dist/100)/10+"km"; }
+            else { dist=Math.round(dist/1000)+"km"; }
+            // Add to table
+            var row=$("<tr class='turn'/>");
+            row.append("<td class='direction i"+step[1]+"'> ");
+            row.append("<td class='instruction'>"+step[2]);
+            row.append("<td class='distance'>"+dist);
+            with ({ num: i, ll: step[0] }) {
+                row.on('click',function(e) { r.clickTurn(num, ll); });
+            };
+            $('#turnbyturn').append(row);
+            cumulative+=step[3];
+        }
+        $('#sidebar_content').append('<p id="routing_credit">' + r.chosenEngine.creditline + '</p>');
+
+    };
+    r.clickTurn=function(num,latlng) {
+        r.popup=L.popup().setLatLng(latlng).setContent("<p>"+(num+1)+"</p>").openOn(r.map);
+    };
+    r.formatDistance=function(m) {
+        if      (m < 1000 ) { return Math.round(m) + "m"; }
+        else if (m < 10000) { return (m/1000.0).toFixed(1) + "km"; }
+        else                { return Math.round(m / 1000)  + "km"; }
+    };
+    r.formatTime=function(s) {
+        var d=new Date(s*1000); var h=d.getHours(); var m=d.getMinutes();
+        return h+":"+(m<10 ? '0' : '')+m;
+    };
+
+    // Close all routing UI
+    
+    r.close=function() {
+        $("#content").addClass("overlay-sidebar");
+        r.route_from=r.route_to=null;
+        $(".query_wrapper.routing input").val("");
+        var remove=['polyline','popup','marker_from','marker_to'];
+        for (var i=0; i<remove.length; i++) {
+            if (r[remove[i]]) { map.removeLayer(r[remove[i]]); r[remove[i]]=null; }
+        }
+    };
+
+    // Routing engine handling
+
+    // Add all engines
+    var list=OSM.RoutingEngines.list;
+    list.sort(function(a,b) { return I18n.t(a.name)>I18n.t(b.name); });
+    var select=r.jqSearch.find('select.routing_engines');
+    for (var i=0; i<list.length; i++) {
+        // Set up JSONP callback
+        with ({num: i}) {
+            list[num].requestJSONP=function(url) {
+                var script = document.createElement('script');
+                script.src = url+r.name+".gotRoute"+num;
+                document.body.appendChild(script); 
+            };
+            list[num].requestCORS=function(url) {
+                $.ajax({ url: url, method: "GET", data: {}, dataType: 'json', success: r['gotRoute'+num] });
+            };
+            r['gotRoute'+num]=function(data) { 
+                r.awaitingRoute=false;
+                $(".query_wrapper.routing .spinner").hide();
+                if (!list[num].gotRoute(r,data)) {
+                    // No route found
+                    if (r.polyline) {
+                        map.removeLayer(r.polyline);
+                        r.polyline=null;
+                    }
+                    if (!r.dragging) { alert(I18n.t('javascripts.directions.errors.no_route')); }
+                }
+            };
+        }
+        select.append("<option value='"+i+"'>"+I18n.t(list[i].name)+"</option>");
+    }
+    r.engines=list;
+    r.chosenEngine=list[0]; // default to first engine
+
+    // Choose an engine on dropdown change
+    r.selectEngine=function(e) {
+        r.chosenEngine=r.engines[e.target.selectedIndex];
+        if (r.polyline){ // and if a route is currently showing, must also refresh, else confusion
+            r.requestRoute(true, false);
+        }
+    };
+    // Choose an engine by name
+    r.chooseEngine=function(name) {
+        for (var i=0; i<r.engines.length; i++) {
+            if (r.engines[i].name==name) {
+                r.chosenEngine=r.engines[i];
+                r.jqSearch.find('select.routing_engines').val(i);
+            }
+        }
+    };
+
+    return r;
+};
diff --git a/app/assets/javascripts/routing_engines/graphhopper.js b/app/assets/javascripts/routing_engines/graphhopper.js
new file mode 100644 (file)
index 0000000..5c10433
--- /dev/null
@@ -0,0 +1,63 @@
+GraphHopperEngine = function(vehicleName, vehicleParam, locale) {
+    this.vehicleName = vehicleName;
+    this.vehicleParam = vehicleParam;
+    this.locale = locale;
+    if (!locale)
+        this.locale = "en";
+};
+
+GraphHopperEngine.prototype.createConfig = function() {
+    var that = this;
+    return {
+        name: "javascripts.directions.engines.graphhopper_"+this.vehicleName.toLowerCase(),
+        draggable: false,
+        _hints: {},
+        getRoute: function(isFinal, points) {
+            var url = "http://graphhopper.com/routing/api/route?" 
+                    + that.vehicleParam 
+                    + "&locale=" + that.locale;
+            for (var i = 0; i < points.length; i++) {
+                var pair = points[i].join(',');
+                url += "&point=" + pair;
+            }            
+            if (isFinal)
+                url += "&instructions=true";
+            // GraphHopper supports json too
+            this.requestJSONP(url + "&type=jsonp&callback=");
+        },
+        gotRoute: function(router, data) {
+            if (!data.info.routeFound) {
+                return false;
+            }
+            // Draw polyline
+            var line = L.PolylineUtil.decode(data.route.coordinates);
+            router.setPolyline(line);
+            // Assemble instructions
+            var steps = [];
+            var instr = data.route.instructions;
+            for (i = 0; i < instr.descriptions.length; i++) {
+                var indi = instr.indications[i];
+                var instrCode = (i == instr.descriptions.length - 1) ? 15 : this.GH_INSTR_MAP[indi];
+                var instrText = "<b>" + (i + 1) + ".</b> ";
+                instrText += instr.descriptions[i];
+                var latlng = instr.latLngs[i];
+                var distInMeter = instr.distances[i];
+                steps.push([{lat: latlng[0], lng: latlng[1]}, instrCode, instrText, distInMeter]);
+            }
+            router.setItinerary({ steps: steps, distance: data.route.distance, time: data.route['time']/1000 });
+            return true;
+        },
+        GH_INSTR_MAP: {
+            "-3": 6, // sharp left
+            "-2": 7, // left   
+            "-1": 8, // slight left                
+            0: 0, // straight
+            1: 1, // slight right
+            2: 2, // right
+            3: 3 // sharp right                
+        }
+    };
+};
+
+OSM.RoutingEngines.list.push(new GraphHopperEngine("Bicycle", "vehicle=bike").createConfig());
+OSM.RoutingEngines.list.push(new GraphHopperEngine("Foot", "vehicle=foot").createConfig());
diff --git a/app/assets/javascripts/routing_engines/mapquest.js b/app/assets/javascripts/routing_engines/mapquest.js
new file mode 100644 (file)
index 0000000..eb92893
--- /dev/null
@@ -0,0 +1,81 @@
+// For docs, see:
+// http://developer.mapquest.com/web/products/open/directions-service
+// http://open.mapquestapi.com/directions/
+// https://github.com/apmon/openstreetmap-website/blob/21edc353a4558006f0ce23f5ec3930be6a7d4c8b/app/controllers/routing_controller.rb#L153
+
+MapQuestEngine = function(vehicleName, vehicleParam, locale) {
+    this.vehicleName = vehicleName;
+    this.vehicleParam = vehicleParam;
+    this.locale = locale;
+    if (!locale)
+        this.locale = "en";
+};
+
+MapQuestEngine.prototype.createConfig = function() {
+    var that = this;
+    return {
+        name: "javascripts.directions.engines.mapquest_"+this.vehicleName.toLowerCase(),
+        creditline: 'Directions courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">',
+        draggable: false,
+        _hints: {},
+        MQ_SPRITE_MAP: {
+            0: 1, // straight
+            1: 2, // slight right
+            2: 3, // right
+            3: 4, // sharp right
+            4: 5, // reverse
+            5: 6, // sharp left
+            6: 7, // left
+            7: 8, // slight left
+            8: 5, // right U-turn
+            9: 5, // left U-turn
+            10: 2, // right merge
+            11: 8, // left merge
+            12: 2, // right on-ramp
+            13: 8, // left on-ramp
+            14: 2, // right off-ramp
+            15: 8, // left off-ramp
+            16: 2, // right fork
+            17: 8, // left fork
+            18: 1  // straight fork
+        },
+        getRoute: function(isFinal,points) {
+            var url="http://open.mapquestapi.com/directions/v2/route?key=Fmjtd%7Cluur290anu%2Crl%3Do5-908a0y";
+            var from=points[0]; var to=points[points.length-1];
+            url+="&from="+from.join(',');
+            url+="&to="+to.join(',');
+            url+="&"+that.vehicleParam;
+            //url+="&locale=" + I18n.currentLocale(); //Doesn't actually work. MapQuest requires full locale e.g. "de_DE", but I18n only provides language, e.g. "de"
+            url+="&manMaps=false";
+            url+="&shapeFormat=raw&generalize=0&unit=k";
+            this.requestCORS(url);
+        },
+        gotRoute: function(router,data) {
+            if (data.info.statuscode!=0) return false;
+        
+            var poly=[];
+            var shape=data.route.shape.shapePoints;
+            for (var i=0; i<shape.length; i+=2) {
+                poly.push(L.latLng(shape[i],shape[i+1]));
+            }
+            router.setPolyline(poly);
+
+            // data.shape.maneuverIndexes links turns to polyline positions
+            // data.legs[0].maneuvers is list of turns
+            var steps=[];
+            var mq=data.route.legs[0].maneuvers;
+            for (var i=0; i<mq.length; i++) {
+                var s=mq[i];
+                var d=(i==mq.length-1) ? 15: this.MQ_SPRITE_MAP[s.turnType];
+                steps.push([L.latLng(s.startPoint.lat, s.startPoint.lng), d, s.narrative, s.distance*1000]);
+            }
+            router.setItinerary( { steps: steps, distance: data.route.distance*1000, time: data.route['time'] });
+            return true;
+        }
+    };
+};
+
+OSM.RoutingEngines.list.push(new MapQuestEngine("Bicycle", "routeType=bicycle").createConfig());
+OSM.RoutingEngines.list.push(new MapQuestEngine("Foot", "routeType=pedestrian").createConfig());
+OSM.RoutingEngines.list.push(new MapQuestEngine("Car", "routeType=fastest").createConfig());
+// can be: routeType=fastest|shortest|pedestrian|multimodal|bicycle
diff --git a/app/assets/javascripts/routing_engines/osrm.js b/app/assets/javascripts/routing_engines/osrm.js
new file mode 100644 (file)
index 0000000..d6f3535
--- /dev/null
@@ -0,0 +1,54 @@
+// OSRM car engine
+// Doesn't yet support hints
+
+OSRMEngine = function(vehicleName, baseURL, locale) {
+    this.vehicleName = vehicleName;
+    this.baseURL = baseURL;
+    this.locale = locale;
+    if (!locale)
+        this.locale = "en";
+};
+
+OSRMEngine.prototype.createConfig = function() {
+    var that = this;
+    return {
+        name: "javascripts.directions.engines.osrm_"+this.vehicleName.toLowerCase(),
+        creditline: 'Directions courtesy of <a href="http://project-osrm.org/" target="_blank">OSRM</a>',
+        draggable: true,
+        _hints: {},
+        getRoute: function(isFinal,points) {
+            var url=that.baseURL+"?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 (isFinal) url+="&instructions=true";
+            this.requestCORS(url);
+        },
+        gotRoute: function(router,data) {
+            if (data.status==207) {
+                return false;
+            }
+            // Draw polyline
+            var line=L.PolylineUtil.decode(data.route_geometry);
+            for (i=0; i<line.length; i++) { line[i].lat/=10; line[i].lng/=10; }
+            router.setPolyline(line);
+            // Assemble instructions
+            var steps=[];
+            for (i=0; i<data.route_instructions.length; i++) {
+                var s=data.route_instructions[i];
+                var instCodes=s[0].split('-');
+                var instText="<b>"+(i+1)+".</b> ";
+                instText+=TURN_INSTRUCTIONS[instCodes[0]];
+                if (instCodes[1]) { instText+="exit "+instCodes[1]+" "; }
+                if (instCodes[0]!=15) { instText+=s[1] ? "<b>"+s[1]+"</b>" : I18n.t('javascripts.directions.instructions.unnamed'); }
+                steps.push([line[s[3]], s[0].split('-')[0], instText, s[2]]);
+            }
+            if (steps.length) router.setItinerary({ steps: steps, distance: data.route_summary.total_distance, time: data.route_summary.total_time });
+            return true;
+        }
+    };
+};
+
+OSM.RoutingEngines.list.push(new OSRMEngine("Car", "http://router.project-osrm.org/viaroute").createConfig());
index 533e91c69ec4d681d276efb34bba8e8f2184346b..51f3c7e9350d9967e300d9c0ee0c8e9508334e2d 100644 (file)
@@ -635,6 +635,10 @@ nav.secondary {
       font-size: 13px;
     }
 
+    p {
+      padding: 0 $lineheight $lineheight/4;
+    }
+
     .icon.close {
       float: right;
       cursor: pointer;
@@ -912,6 +916,7 @@ header .search_form {
 .search_form {
   position: relative;
   padding: $lineheight/2;
+  padding-top: 1px;
   background-color: $lightgrey;
 
   .query_wrapper {
@@ -942,13 +947,15 @@ header .search_form {
     border-radius: 0 2px 2px 0;
   }
 
-  .describe_location {
-    position: absolute;
-    top: 6px;
-    right: 6px;
+  .query_options {
+    text-align: right;
     font-size: 10px;
     color: $blue;
   }
+
+  .routing {
+    display: none;
+  }
 }
 
 /* Rules for the map key which appears in the popout sidebar */
@@ -985,6 +992,41 @@ header .search_form {
   color: #f00;
 }
 
+/* Rules for routing */
+
+#sidebar_content>table {
+    padding: 5px 20px 10px 15px;
+    width: 100%;
+    border-collapse: separate;
+}
+
+td.direction {
+    background-image: image-url('routing-sprite.png'); 
+    width: 20px; height: 20px; 
+    background-repeat: no-repeat;
+}
+@for $i from 1 through 17 {
+td.direction.i#{$i}  { background-position: #{($i)*-20+20}px 0px; }
+}
+
+td.instruction, td.distance {
+    padding-top: 0;
+    border-bottom: 1px solid #DDD;
+}
+td.distance {
+    color: #BBB;
+    text-align: right;
+    font-size: x-small;
+}
+tr.turn {
+    cursor: pointer;
+}
+tr.turn:hover {
+    background: lighten($green, 30%); 
+}
+.routing_engines, #route_from, #route_to { margin-left: 25px; }
+.routing_marker { width: 15px; position: absolute; }
+
 /* Rules for entity history */
 
 #sidebar_content {
@@ -1175,6 +1217,15 @@ header .search_form {
   }
 }
 
+/* Rules for the routing sidebar */
+
+#sidebar_content {
+  #routing_credit {
+    text-align: center;
+    padding: 0.5em;
+  }
+}
+
 /* Rules for edit pages */
 
 .site-edit {
index ed548e21c78c8671918b7f5223a2a21ceb597030..9ac3eae33ab244c2854755714e7104f8922d8a8d 100644 (file)
@@ -1,7 +1,28 @@
 <form method="GET" action="<%= search_path %>" class="search_form">
+
+  <div class='query_options search'>
+    <%= link_to t('site.search.where_am_i'), '#', { :class => "describe_location", :title => t('site.search.where_am_i_title') } %>
+    &middot;
+    <%= link_to t('site.search.get_directions'), '#', { :class => "get_directions", :title => t('site.search.get_directions_title') } %>
+  </div>
+
+  <div class='query_options routing'>
+    <%= link_to t('site.search.close_directions'), '#', { :class => "close_directions", :title => t('site.search.close_directions_title') } %>
+  </div>
+
   <%= 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>
+
+  <div class='query_wrapper routing'>
+    <%= image_tag "marker-green.png", :class => 'routing_marker', :id => 'marker_from', :draggable => 'true' %>
+    <%= text_field_tag "route_from", params[:from], :placeholder => t('site.search.from'), :onchange=>"OSM.routing.geocode('route_from',event)" %>
+    <%= image_tag "marker-red.png"  , :class => 'routing_marker', :id => 'marker_to'  , :draggable => 'true' %>
+    <%= text_field_tag "route_to"  , params[:to]  , :placeholder => t('site.search.to')  , :onchange=>"OSM.routing.geocode('route_to'  ,event)" %>
+    <select class='routing_engines' name='routing_engines' onchange="OSM.routing.selectEngine(event)"></select>
+    <%= image_tag "searching-small.gif", :class => 'spinner', :style => "vertical-align: middle; display: none;" %>
+  </div>
+
 </form>
index c4910d86f1b6f555b7ecbe727b6847b6cb7fcc6e..36f71792f40e381d23f1f419a5778f8e08875a7b 100644 (file)
@@ -69,7 +69,7 @@ OpenStreetMap::Application.configure do
 
   # Precompile additional assets.
   # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
-  config.assets.precompile += %w( index.js browse.js welcome.js fixthemap.js )
+  config.assets.precompile += %w( index.js browse.js welcome.js fixthemap.js routing.js )
   config.assets.precompile += %w( user.js diary_entry.js swfobject.js )
   config.assets.precompile += %w( large-ltr.css small-ltr.css print-ltr.css )
   config.assets.precompile += %w( large-rtl.css small-rtl.css print-rtl.css )
index 2f79730d28e8d6be139993fc8fc0774a20df5264..d2d9e9349d59c244e0a02ac4aa58b6d6ab98a8dc 100644 (file)
@@ -941,6 +941,32 @@ de:
   javascripts: 
     close: Schließen
     edit_help: Wähle eine höhere Zoomstufe und verschiebe die Karte an einen Ort, den du bearbeiten möchtest, und klicke hier.
+    directions:
+      directions: "Fahranweisungen: "
+      engines:
+        graphhopper_bike: "Fahrrad (GraphHopper)"
+        mapquest_bike: "Fahrrad (MapQuest)"
+        osrm_car: "Auto (OSRM)"
+        cloudmade_foot: "Fuss (Cloudmade)"
+      instructions:
+        continue_on: "Weiter auf "
+        slight_right: "Rechts halten auf "
+        turn_right: "Rechts abbiegen auf "
+        sharp_right: "Hart rechts auf "
+        uturn: "U-turn along "
+        sharp_left: "Hart links auf "
+        turn_left: "Links abbiegen auf "
+        slight_left: "Links halten auf "
+        via_point: "(via point) "
+        follow:  "Folge "
+        roundabout: "Im Kreisverkehr nehme "
+        leave_roundabout: "Verlasse den Kreisverkehr - "
+        stay_roundabout: "Stay on roundabout - "
+        start: "Start at end of "
+        destination: "Ziel erreicht"
+        against_oneway: "Go against one-way on "
+        end_oneway: "Ende der Einbahnstrasse "
+        unnamed: "(unbekannt)"
     key: 
       title: Legende
       tooltip: Legende
@@ -1479,6 +1505,12 @@ de:
       preview: Vorschau
     search: 
       search: Suchen
+      get_directions: "Route berechnen"
+      get_directions_title: "Routenberechnung zwischen zwei Orten"
+      close_directions: "Schliessen der Route"
+      close_directions_title: "Schliessen des Routenmenus"
+      from: "Von"
+      to: "Nach"
       submit_text: Los
       where_am_i: Wo bin ich?
       where_am_i_title: Die momentane Position mit der Suchmaschine anzeigen
index 94537c1fb40d0ce1654185e32fe613ca2ac70a02..4384fda865930c7249da06cdb449b73384bdaa99 100644 (file)
@@ -1325,6 +1325,12 @@ en:
       close: Close
     search:
       search: Search
+      get_directions: "Get directions"
+      get_directions_title: "Find directions between two points"
+      close_directions: "Close directions"
+      close_directions_title: "Close the directions panel"
+      from: "From"
+      to: "To"
       where_am_i: "Where am I?"
       where_am_i_title: Describe the current location using the search engine
       submit_text: "Go"
@@ -2113,6 +2119,39 @@ en:
         comment_and_resolve: Comment & Resolve
         comment: Comment
     edit_help: Move the map and zoom in on a location you want to edit, then click here.
+    directions:
+      engines:
+        graphhopper_bicycle: "Bicycle (GraphHopper)"
+        graphhopper_foot: "Foot (GraphHopper)"
+        mapquest_bicycle: "Bicycle (MapQuest)"
+        mapquest_car: "Car (MapQuest)"
+        mapquest_foot: "Foot (MapQuest)"
+        osrm_car: "Car (OSRM)"
+      directions: "Directions"
+      distance: "Distance"
+      errors:
+        no_route: "Couldn't find a route between those two places."
+        no_place: "Sorry - couldn't find that place."
+      instructions:
+        continue_on: "Continue on "
+        slight_right: "Slight right onto "
+        turn_right: "Turn right onto "
+        sharp_right: "Sharp right onto "
+        uturn: "U-turn along "
+        sharp_left: "Sharp left onto "
+        turn_left: "Turn left onto "
+        slight_left: "Slight left onto "
+        via_point: "(via point) "
+        follow:  "Follow "
+        roundabout: "At roundabout take "
+        leave_roundabout: "Leave roundabout - "
+        stay_roundabout: "Stay on roundabout - "
+        start: "Start at end of "
+        destination: "Reach destination"
+        against_oneway: "Go against one-way on "
+        end_oneway: "End of one-way on "
+        unnamed: "(unnamed)"
+      time: "Time"
   redaction:
     edit:
       description: "Description"
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