From: Tom Hughes Date: Sun, 1 Feb 2015 11:32:02 +0000 (+0000) Subject: Merge branch 'master' into routing X-Git-Tag: live~4217^2~5^2~7 X-Git-Url: https://git.openstreetmap.org/rails.git/commitdiff_plain/13d650e2dcbe2467a82253097e10d58dddf9d9d2?hp=959a076b50b5bce5e5d0360c80b59cbe62e3955f Merge branch 'master' into routing --- diff --git a/app/assets/images/directions.png b/app/assets/images/directions.png new file mode 100644 index 000000000..197244f4a Binary files /dev/null and b/app/assets/images/directions.png differ diff --git a/app/assets/images/routing-sprite.png b/app/assets/images/routing-sprite.png new file mode 100644 index 000000000..37d94886d Binary files /dev/null and b/app/assets/images/routing-sprite.png differ diff --git a/app/assets/images/search.png b/app/assets/images/search.png new file mode 100644 index 000000000..15cc32e47 Binary files /dev/null and b/app/assets/images/search.png differ diff --git a/app/assets/images/searching-small.gif b/app/assets/images/searching-small.gif new file mode 100644 index 000000000..06dbc2bc2 Binary files /dev/null and b/app/assets/images/searching-small.gif differ diff --git a/app/assets/javascripts/index.js b/app/assets/javascripts/index.js index df975db48..afb6b3d65 100644 --- a/app/assets/javascripts/index.js +++ b/app/assets/javascripts/index.js @@ -5,6 +5,7 @@ //= require leaflet.key //= require leaflet.note //= require leaflet.share +//= require leaflet.polyline //= require leaflet.query //= require index/search //= require index/browse @@ -13,14 +14,17 @@ //= require index/history //= require index/note //= require index/new_note +//= require index/directions //= require index/changeset //= require index/query //= require router -(function() { +$(document).ready(function () { var loaderTimeout; OSM.loadSidebarContent = function(path, callback) { + map.setSidebarOverlaid(false); + clearTimeout(loaderTimeout); loaderTimeout = setTimeout(function() { @@ -68,9 +72,7 @@ } }); }; -})(); -$(document).ready(function () { var params = OSM.mapParams(); var map = new L.OSM.Map("map", { @@ -228,32 +230,22 @@ $(document).ready(function () { OSM.Index = function(map) { var page = {}; - page.pushstate = function() { - $("#content").addClass("overlay-sidebar"); - map.invalidateSize({pan: false}) - .panBy([-350, 0], {animate: false}); + page.pushstate = page.popstate = function() { + map.setSidebarOverlaid(true); document.title = I18n.t('layouts.project_name.title'); }; page.load = function() { + var params = querystring.parse(location.search.substring(1)); + if (params.query) { + $("#sidebar .search_form input[name=query]").value(params.query); + } if (!("autofocus" in document.createElement("input"))) { $("#sidebar .search_form input[name=query]").focus(); } return map.getState(); }; - page.popstate = function() { - $("#content").addClass("overlay-sidebar"); - map.invalidateSize({pan: false}); - document.title = I18n.t('layouts.project_name.title'); - }; - - page.unload = function() { - map.panBy([350, 0], {animate: false}); - $("#content").removeClass("overlay-sidebar"); - map.invalidateSize({pan: false}); - }; - return page; }; @@ -293,6 +285,7 @@ $(document).ready(function () { OSM.router = OSM.Router(map, { "/": OSM.Index(map), "/search": OSM.Search(map), + "/directions": OSM.Directions(map), "/export": OSM.Export(map), "/note/new": OSM.NewNote(map), "/history/friends": history, diff --git a/app/assets/javascripts/index/directions.js.erb b/app/assets/javascripts/index/directions.js.erb new file mode 100644 index 000000000..93767612b --- /dev/null +++ b/app/assets/javascripts/index/directions.js.erb @@ -0,0 +1,379 @@ +//= require_self +//= require_tree ./directions_engines + +OSM.Directions = function (map) { + var awaitingGeocode; // true if the user has requested a route, but we're waiting on a geocode result + var awaitingRoute; // true if we've asked the engine for a route and are waiting to hear back + var dragging; // true if the user is dragging a start/end point + var chosenEngine; + + var popup = L.popup(); + + var polyline = L.polyline([], { + color: '#03f', + opacity: 0.3, + weight: 10 + }); + + var highlight = L.polyline([], { + color: '#ff0', + opacity: 0.5, + weight: 12 + }); + + var endpoints = [ + Endpoint($("input[name='route_from']"), <%= asset_path('marker-green.png').to_json %>), + Endpoint($("input[name='route_to']"), <%= asset_path('marker-red.png').to_json %>) + ]; + + function Endpoint(input, iconUrl) { + var endpoint = {}; + + endpoint.marker = L.marker([0, 0], { + icon: L.icon({ + iconUrl: iconUrl, + iconSize: [25, 41], + iconAnchor: [12, 41], + popupAnchor: [1, -34], + shadowUrl: <%= asset_path('images/marker-shadow.png').to_json %>, + shadowSize: [41, 41] + }), + draggable: true + }); + + endpoint.marker.on('drag dragend', function (e) { + dragging = (e.type == 'drag'); + if (dragging && !chosenEngine.draggable) return; + if (dragging && awaitingRoute) return; + endpoint.setLatLng(e.target.getLatLng()); + if (map.hasLayer(polyline)) { + getRoute(); + } + }); + + input.on("change", function (e) { + // make text the same in both text boxes + var value = e.target.value; + endpoint.setValue(value) + endpoint.getGeocode(); + }); + + endpoint.setValue = function(value) { + endpoint.value = value; + input.val(value); + } + + endpoint.getGeocode = function() { + // if no one has entered a value yet, then we can't geocode, so don't + // even try. + if (!endpoint.value) { + return; + } + + endpoint.awaitingGeocode = true; + + $.getJSON('<%= NOMINATIM_URL %>search?q=' + encodeURIComponent(endpoint.value) + '&format=json', function (json) { + endpoint.awaitingGeocode = false; + endpoint.hasGeocode = true; + if (json.length == 0) { + alert(I18n.t('javascripts.directions.errors.no_place')); + return; + } + + input.val(json[0].display_name); + + endpoint.latlng = L.latLng(json[0]); + endpoint.marker + .setLatLng(endpoint.latlng) + .addTo(map); + + if (awaitingGeocode) { + awaitingGeocode = false; + getRoute(); + } + }); + } + + endpoint.setLatLng = function (ll) { + var precision = OSM.zoomPrecision(map.getZoom()); + input.val(ll.lat.toFixed(precision) + ", " + ll.lng.toFixed(precision)); + endpoint.hasGeocode = true; + endpoint.latlng = ll; + endpoint.marker + .setLatLng(ll) + .addTo(map); + }; + + return endpoint; + } + + $(".directions_form a.directions_close").on("click", function(e) { + e.preventDefault(); + var route_from = endpoints[0].value; + if (route_from) { + OSM.router.route("/?query=" + encodeURIComponent(route_from) + OSM.formatHash(map)); + } else { + OSM.router.route("/" + OSM.formatHash(map)); + } + }); + + function formatDistance(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"; + } + } + + function formatTime(s) { + var m = Math.round(s / 60); + var h = Math.floor(m / 60); + m -= h * 60; + return h + ":" + (m < 10 ? '0' : '') + m; + } + + function setEngine(id) { + engines.forEach(function(engine, i) { + if (engine.id == id) { + chosenEngine = engine; + select.val(i); + } + }); + } + + function getRoute() { + // go fetch geocodes for any endpoints which have not already + // been geocoded. + for (var ep_i = 0; ep_i < 2; ++ep_i) { + var endpoint = endpoints[ep_i]; + if (!endpoint.hasGeocode && !endpoint.awaitingGeocode) { + endpoint.getGeocode(); + awaitingGeocode = true; + } + } + if (endpoints[0].awaitingGeocode || endpoints[1].awaitingGeocode) { + awaitingGeocode = true; + return; + } + + var o = endpoints[0].latlng, + d = endpoints[1].latlng; + + if (!o || !d) return; + + var precision = OSM.zoomPrecision(map.getZoom()); + + OSM.router.replace("/directions?" + querystring.stringify({ + engine: chosenEngine.id, + route: o.lat.toFixed(precision) + ',' + o.lng.toFixed(precision) + ';' + + d.lat.toFixed(precision) + ',' + d.lng.toFixed(precision) + })); + + // copy loading item to sidebar and display it. we copy it, rather than + // just using it in-place and replacing it in case it has to be used + // again. + $('#sidebar_content').html($('.directions_form .loader_copy').html()); + awaitingRoute = true; + map.setSidebarOverlaid(false); + + chosenEngine.getRoute([o, d], function (err, route) { + awaitingRoute = false; + + if (err) { + map.removeLayer(polyline); + + if (!dragging) { + alert(I18n.t('javascripts.directions.errors.no_route')); + } + + return; + } + + polyline + .setLatLngs(route.line) + .addTo(map); + + if (!dragging) { + map.fitBounds(polyline.getBounds().pad(0.05)); + } + + var html = '

' + + '' + I18n.t('javascripts.directions.directions') + + '

' + + I18n.t('javascripts.directions.distance') + ': ' + formatDistance(route.distance) + '. ' + + I18n.t('javascripts.directions.time') + ': ' + formatTime(route.time) + '.

' + + ''; + + $('#sidebar_content') + .html(html); + + // Add each row + var cumulative = 0; + route.steps.forEach(function (step) { + var ll = step[0], + direction = step[1], + instruction = step[2], + dist = step[3], + lineseg = step[4]; + + cumulative += dist; + + 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"; + } + + var row = $(""); + row.append(" "); + row.append("
" + instruction); + row.append("" + dist); + + row.on('click', function () { + popup + .setLatLng(ll) + .setContent("

" + instruction + "

") + .openOn(map); + }); + + row.hover(function () { + highlight + .setLatLngs(lineseg) + .addTo(map); + }, function () { + map.removeLayer(highlight); + }); + + $('#turnbyturn').append(row); + }); + + $('#sidebar_content').append('

' + + I18n.t('javascripts.directions.instructions.courtesy', {link: chosenEngine.creditline}) + + '

'); + + $('#sidebar_content a.geolink').on('click', function(e) { + e.preventDefault(); + map.removeLayer(polyline); + $('#sidebar_content').html(''); + map.setSidebarOverlaid(true); + // TODO: collapse width of sidebar back to previous + }); + }); + } + + var engines = OSM.Directions.engines; + + engines.sort(function (a, b) { + a = I18n.t('javascripts.directions.engines.' + a.id); + b = I18n.t('javascripts.directions.engines.' + b.id); + return a.localeCompare(b); + }); + + var select = $('select.routing_engines'); + + engines.forEach(function(engine, i) { + select.append(""); + }); + + setEngine('osrm_car'); + + select.on("change", function (e) { + chosenEngine = engines[e.target.selectedIndex]; + if (map.hasLayer(polyline)) { + getRoute(); + } + }); + + $(".directions_form").on("submit", function(e) { + e.preventDefault(); + $("header").addClass("closed"); + getRoute(); + }); + + $(".routing_marker").on('dragstart', function (e) { + e.originalEvent.dataTransfer.effectAllowed = 'move'; + e.originalEvent.dataTransfer.setData('id', this.id); + var xo = e.originalEvent.clientX - $(e.target).offset().left; + var yo = e.originalEvent.clientY - $(e.target).offset().top; + e.originalEvent.dataTransfer.setData('offsetX', e.originalEvent.target.width / 2 - xo); + e.originalEvent.dataTransfer.setData('offsetY', e.originalEvent.target.height - yo); + }); + + var page = {}; + + page.pushstate = page.popstate = function() { + $(".search_form").hide(); + $(".directions_form").show(); + + $("#map").on('dragend dragover', function (e) { + e.preventDefault(); + }); + + $("#map").on('drop', function (e) { + e.preventDefault(); + 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); + endpoints[id === 'marker_from' ? 0 : 1].setLatLng(ll); + getRoute(); + }); + + var params = querystring.parse(location.search.substring(1)), + route = (params.route || '').split(';'); + + if (params.engine) { + setEngine(params.engine); + } + + if (params.from) { + endpoints[0].setValue(params.from); + } + + var o = route[0] && L.latLng(route[0].split(',')), + d = route[1] && L.latLng(route[1].split(',')); + + if (o) endpoints[0].setLatLng(o); + if (d) endpoints[1].setLatLng(d); + + map.setSidebarOverlaid(!o || !d); + + getRoute(); + }; + + page.load = function() { + page.pushstate(); + }; + + page.unload = function() { + $(".search_form").show(); + $(".directions_form").hide(); + $("#map").off('dragend dragover drop'); + + map + .removeLayer(popup) + .removeLayer(polyline) + .removeLayer(endpoints[0].marker) + .removeLayer(endpoints[1].marker); + }; + + return page; +}; + +OSM.Directions.engines = []; + +OSM.Directions.addEngine = function (engine, supportsHTTPS) { + if (document.location.protocol == "http:" || supportsHTTPS) { + OSM.Directions.engines.push(engine); + } +}; diff --git a/app/assets/javascripts/index/directions_engines/graphhopper.js b/app/assets/javascripts/index/directions_engines/graphhopper.js new file mode 100644 index 000000000..8d01bcd16 --- /dev/null +++ b/app/assets/javascripts/index/directions_engines/graphhopper.js @@ -0,0 +1,74 @@ +function GraphHopperEngine(id, vehicleParam) { + var 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 + 4: -1, // finish reached + 5: -1 // via reached + }; + + return { + id: id, + creditline: 'Graphhopper', + draggable: false, + + getRoute: function (points, callback) { + // documentation + // https://github.com/graphhopper/graphhopper/blob/master/docs/web/api-doc.md + var url = "https://graphhopper.com/api/1/route?" + + vehicleParam + + "&locale=" + I18n.currentLocale() + + "&key=LijBPDQGfu7Iiq80w3HzwB4RUDJbMbhs6BU0dEnn" + + "&type=jsonp" + + "&instructions=true"; + + for (var i = 0; i < points.length; i++) { + url += "&point=" + points[i].lat + ',' + points[i].lng; + } + + $.ajax({ + url: url, + dataType: 'jsonp', + success: function (data) { + if (!data.paths || data.paths.length == 0) + return callback(true); + + var path = data.paths[0]; + var line = L.PolylineUtil.decode(path.points); + + var steps = []; + var len = path.instructions.length; + for (var i = 0; i < len; i++) { + var instr = path.instructions[i]; + var instrCode = (i === len - 1) ? 15 : GH_INSTR_MAP[instr.sign]; + var instrText = "" + (i + 1) + ". "; + instrText += instr.text; + var latLng = line[instr.interval[0]]; + var distInMeter = instr.distance; + steps.push([ + {lat: latLng.lat, lng: latLng.lng}, + instrCode, + instrText, + distInMeter, + [] + ]); // TODO does graphhopper map instructions onto line indices? + } + + callback(null, { + line: line, + steps: steps, + distance: path.distance, + time: path.time / 1000 + }); + } + }); + } + }; +} + +OSM.Directions.addEngine(GraphHopperEngine("graphhopper_bicycle", "vehicle=bike"), false); +OSM.Directions.addEngine(GraphHopperEngine("graphhopper_foot", "vehicle=foot"), false); diff --git a/app/assets/javascripts/index/directions_engines/mapquest.js b/app/assets/javascripts/index/directions_engines/mapquest.js new file mode 100644 index 000000000..935a1ca4c --- /dev/null +++ b/app/assets/javascripts/index/directions_engines/mapquest.js @@ -0,0 +1,94 @@ +// 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 + +function MapQuestEngine(id, vehicleParam) { + var 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 + }; + + return { + id: id, + creditline: 'MapQuest ', + draggable: false, + + getRoute: function (points, callback) { + var url = document.location.protocol + "//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.lat + ',' + from.lng; + url += "&to=" + to.lat + ',' + to.lng; + url += "&" + vehicleParam; + //url+="&locale=" + I18n.currentLocale(); //Doesn't actually work. MapQuest requires full locale e.g. "de_DE", but I18n may only provides language, e.g. "de" + url += "&manMaps=false"; + url += "&shapeFormat=raw&generalize=0&unit=k"; + + $.ajax({ + url: url, + success: function (data) { + if (data.info.statuscode != 0) + return callback(true); + + var line = []; + var shape = data.route.shape.shapePoints; + for (var i = 0; i < shape.length; i += 2) { + line.push(L.latLng(shape[i], shape[i + 1])); + } + + // data.route.shape.maneuverIndexes links turns to polyline positions + // data.route.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; + var linesegstart, linesegend, lineseg; + linesegstart = data.route.shape.maneuverIndexes[i]; + if (i == mq.length - 1) { + d = 15; + linesegend = linesegstart + 1; + } else { + d = MQ_SPRITE_MAP[s.turnType]; + linesegend = data.route.shape.maneuverIndexes[i + 1] + 1; + } + lineseg = []; + for (var j = linesegstart; j < linesegend; j++) { + lineseg.push(L.latLng(data.route.shape.shapePoints[j * 2], data.route.shape.shapePoints[j * 2 + 1])); + } + steps.push([L.latLng(s.startPoint.lat, s.startPoint.lng), d, s.narrative, s.distance * 1000, lineseg]); + } + + callback(null, { + line: line, + steps: steps, + distance: data.route.distance * 1000, + time: data.route['time'] + }); + } + }); + } + }; +} + +OSM.Directions.addEngine(MapQuestEngine("mapquest_bicycle", "routeType=bicycle"), true); +OSM.Directions.addEngine(MapQuestEngine("mapquest_foot", "routeType=pedestrian"), true); +OSM.Directions.addEngine(MapQuestEngine("mapquest_car", "routeType=fastest"), true); diff --git a/app/assets/javascripts/index/directions_engines/osrm.js b/app/assets/javascripts/index/directions_engines/osrm.js new file mode 100644 index 000000000..69b78c25e --- /dev/null +++ b/app/assets/javascripts/index/directions_engines/osrm.js @@ -0,0 +1,96 @@ +// OSRM car engine +// Doesn't yet support hints + +function OSRMEngine() { + var previousPoints, hintData; + + return { + id: "osrm_car", + creditline: 'OSRM', + draggable: true, + + getRoute: function (points, callback) { + var 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 + ]; + + var url = "http://router.project-osrm.org/viaroute?z=14&output=json&instructions=true"; + + for (var i = 0; i < points.length; i++) { + url += "&loc=" + points[i].lat + ',' + points[i].lng; + if (hintData && previousPoints && previousPoints[i].equals(points[i])) { + url += "&hint=" + hintData.locations[i]; + } + } + + if (hintData && hintData.checksum) { + url += "&checksum=" + hintData.checksum; + } + + $.ajax({ + url: url, + dataType: 'json', + success: function (data) { + if (data.status == 207) + return callback(true); + + previousPoints = points; + hintData = data.hint_data; + + var line = L.PolylineUtil.decode(data.route_geometry); + for (var i = 0; i < line.length; i++) { + line[i].lat /= 10; + line[i].lng /= 10; + } + + var steps = []; + for (i = 0; i < data.route_instructions.length; i++) { + var s = data.route_instructions[i]; + var linesegend; + var instCodes = s[0].split('-'); + var instText = "" + (i + 1) + ". "; + instText += TURN_INSTRUCTIONS[instCodes[0]]; + if (instCodes[1]) { + instText += "exit " + instCodes[1] + " "; + } + if (instCodes[0] != 15) { + instText += s[1] ? "" + s[1] + "" : I18n.t('javascripts.directions.instructions.unnamed'); + } + if ((i + 1) < data.route_instructions.length) { + linesegend = data.route_instructions[i + 1][3] + 1; + } else { + linesegend = s[3] + 1; + } + steps.push([line[s[3]], s[0].split('-')[0], instText, s[2], line.slice(s[3], linesegend)]); + } + + callback(null, { + line: line, + steps: steps, + distance: data.route_summary.total_distance, + time: data.route_summary.total_time + }); + } + }); + } + }; +} + +OSM.Directions.addEngine(OSRMEngine(), false); diff --git a/app/assets/javascripts/index/search.js b/app/assets/javascripts/index/search.js index e9f06ddf0..08d2dc96c 100644 --- a/app/assets/javascripts/index/search.js +++ b/app/assets/javascripts/index/search.js @@ -1,14 +1,42 @@ //= require jquery.simulate OSM.Search = function(map) { - $(".search_form input[name=query]") - .on("input", function(e) { - if ($(e.target).val() == "") { - $(".describe_location").fadeIn(100); - } else { - $(".describe_location").fadeOut(100); - } - }) + $(".search_form input[name=query]").on("input", function(e) { + if ($(e.target).val() == "") { + $(".describe_location").fadeIn(100); + } else { + $(".describe_location").fadeOut(100); + } + }); + + $(".search_form a.button.switch_link").on("click", function(e) { + e.preventDefault(); + var query = $(e.target).parent().parent().find("input[name=query]").val(); + if (query) { + OSM.router.route("/directions?from=" + encodeURIComponent(query) + OSM.formatHash(map)); + } else { + OSM.router.route("/directions" + OSM.formatHash(map)); + } + }); + + $(".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)); + } else { + OSM.router.route("/" + OSM.formatHash(map)); + } + }); + + $(".describe_location").on("click", function(e) { + e.preventDefault(); + var precision = OSM.zoomPrecision(map.getZoom()); + OSM.router.route("/search?query=" + encodeURIComponent( + map.getCenter().lat.toFixed(precision) + "," + + map.getCenter().lng.toFixed(precision))); + }); $("#sidebar_content") .on("click", ".search_more a", clickSearchMore) diff --git a/app/assets/javascripts/leaflet.map.js.erb b/app/assets/javascripts/leaflet.map.js.erb index b4767f96b..14cddafce 100644 --- a/app/assets/javascripts/leaflet.map.js.erb +++ b/app/assets/javascripts/leaflet.map.js.erb @@ -242,6 +242,19 @@ L.OSM.Map = L.Map.extend({ setState: function(state, options) { if (state.center) this.setView(state.center, state.zoom, options); if (state.layers) this.updateLayers(state.layers); + }, + + setSidebarOverlaid: function(overlaid) { + if (overlaid && !$("#content").hasClass("overlay-sidebar")) { + $("#content").addClass("overlay-sidebar"); + this.invalidateSize({pan: false}) + .panBy([-350, 0], {animate: false}); + } else if (!overlaid && $("#content").hasClass("overlay-sidebar")) { + this.panBy([350, 0], {animate: false}); + $("#content").removeClass("overlay-sidebar"); + this.invalidateSize({pan: false}); + } + return this; } }); diff --git a/app/assets/javascripts/router.js b/app/assets/javascripts/router.js index dcf8ea6a8..904134fc0 100644 --- a/app/assets/javascripts/router.js +++ b/app/assets/javascripts/router.js @@ -127,6 +127,10 @@ OSM.Router = function(map, rts) { return true; }; + router.replace = function (url) { + window.history.replaceState(OSM.parseHash(url), document.title, url); + }; + router.stateChange = function(state) { if (state.center) { window.history.replaceState(state, document.title, OSM.formatHash(state)); @@ -135,7 +139,7 @@ OSM.Router = function(map, rts) { } }; } else { - router.route = function (url) { + router.route = router.replace = function (url) { window.location.assign(url); }; diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 36e0278e6..ff16ad66b 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -907,13 +907,15 @@ nav.secondary { } } -/* Rules for the search box */ +/* Rules for the search and direction forms */ -header .search_form { +header .search_forms, +.directions_form { display: none; } -.search_form { +.search_form, +.directions_form { position: relative; padding: $lineheight/2; background-color: $lightgrey; @@ -927,23 +929,37 @@ header .search_form { input[type=text] { width: 100%; height: 30px; - border-right: none; - transition: 300ms linear; } + input[type=text].overflow { + border-right: none; + } + input:focus { outline: none; box-shadow: 0px 0px 7px #9ED485; } - input[type=submit] { + input[type=submit].float { float: right; width: auto; min-width: 0; border-radius: 0 2px 2px 0; } + select { + /* this next line is to polyfill the vertical alignment of text within a select element, + * which is different between firefox and chrome. */ + padding: 0.3em 0; + } + + .query_options { + text-align: right; + font-size: 10px; + color: $blue; + } + .describe_location { position: absolute; top: 6px; @@ -951,6 +967,33 @@ header .search_form { font-size: 10px; color: $blue; } + + .switch_link { + float: right; + width: auto; + min-width: 0; + margin-left: 6px; + } + + img.button { + display: block; + } + + span.force_width { + width: 100%; + padding-right: 25px; + display: block; + } + + select.routing_engines { + min-height: 30px; + margin: 0px 0px 5px 25px; + } + + div.line { + width: 100%; + margin: 0px 0px 5px 0px; + } } /* Rules for the map key which appears in the popout sidebar */ @@ -973,7 +1016,7 @@ header .search_form { border-bottom: $keyline; cursor: pointer; &:first-child { border-top: $keyline; } - &.selected { background: #FFFFE6; } + &.selected { background: $list-highlight; } } .search_details { @@ -989,6 +1032,47 @@ header .search_form { color: #f00; } +/* Rules for routing */ + +#sidebar_content>table { + padding: 5px 20px 10px 15px; + width: 100%; + border-collapse: separate; +} + +div.direction { + background-image: image-url('routing-sprite.png'); + width: 20px; + height: 20px; + background-repeat: no-repeat; +} +@for $i from 1 through 17 { +div.direction.i#{$i} { background-position: #{($i)*-20+20}px 0px; } +} + +p#routing_summary { + padding: 0 $lineheight $lineheight/4; +} + +td.instruction, td.distance { + padding-top: $lineheight/5; + padding-bottom: $lineheight/5; + border-bottom: 1px solid #DDD; +} +td.distance { + color: #BBB; + text-align: right; + font-size: x-small; +} +tr.turn { + cursor: pointer; +} +tr.turn:hover { + background: $list-highlight; +} +.routing_engines, #route_from, #route_to { margin-left: 25px; } +.routing_marker { width: 15px; position: absolute; } + /* Rules for entity history */ #sidebar_content { @@ -1010,7 +1094,7 @@ header .search_form { border-bottom: 1px solid #ddd; cursor: pointer; - &.selected { background: #FFFFE6; } + &.selected { background: $list-highlight; } /* color is derived from changeset bbox fillColor in history.js */ } @@ -1167,7 +1251,7 @@ header .search_form { } &.selected { - background: #FFFFE6; + background: $list-highlight; } } } @@ -1228,6 +1312,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 { diff --git a/app/assets/stylesheets/parameters.scss b/app/assets/stylesheets/parameters.scss index b26b29105..eb363459c 100644 --- a/app/assets/stylesheets/parameters.scss +++ b/app/assets/stylesheets/parameters.scss @@ -15,3 +15,5 @@ $headerHeight: 55px; $sidebarWidth: 350px; $keyline: 1px solid $lightgrey; $border-radius: 3px; +$list-highlight: #FFFFE6; +$border: 1px solid $grey; diff --git a/app/assets/stylesheets/small.scss b/app/assets/stylesheets/small.scss index e9d0a4779..950e1214d 100644 --- a/app/assets/stylesheets/small.scss +++ b/app/assets/stylesheets/small.scss @@ -36,12 +36,12 @@ header { display: none; } - .search_form { + .search_forms { display: block; } } -#sidebar .search_form, +#sidebar .search_forms, #edit_tab, #export_tab { display: none; diff --git a/app/controllers/directions_controller.rb b/app/controllers/directions_controller.rb new file mode 100644 index 000000000..d153f0320 --- /dev/null +++ b/app/controllers/directions_controller.rb @@ -0,0 +1,9 @@ +class DirectionsController < ApplicationController + before_filter :authorize_web + before_filter :set_locale + before_filter :require_oauth, :only => [:search] + + def search + render :layout => map_layout + end +end diff --git a/app/views/directions/search.html.erb b/app/views/directions/search.html.erb new file mode 100644 index 000000000..ea6ee7088 --- /dev/null +++ b/app/views/directions/search.html.erb @@ -0,0 +1 @@ +<% content_for(:content_class) { "overlay-sidebar" } %> diff --git a/app/views/layouts/_search.html.erb b/app/views/layouts/_search.html.erb index ed548e21c..02d6406cf 100644 --- a/app/views/layouts/_search.html.erb +++ b/app/views/layouts/_search.html.erb @@ -1,7 +1,26 @@ -
- <%= submit_tag t('site.search.submit_text') %> -
- <%= 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') } %> -
-
+
+
+ <%= link_to image_tag('directions.png', :class => 'button'), directions_path, { :class => "button switch_link", :title => t('site.search.get_directions_title') } %> + <%= submit_tag t('site.search.submit_text'), :class => 'float' %> +
+ <%= text_field_tag "query", params[:query], :placeholder => t("site.search.search"), :autofocus => autofocus, :class => 'overflow' %> + <%= link_to t('site.search.where_am_i'), '#', { :class => "describe_location", :title => t('site.search.where_am_i_title') } %> +
+
+ +
+
<%= link_to tag('span', { :class => "icon close"}), root_path, { :title => t('site.search.close_directions_title'), :class => "directions_close" } %>
+ +
+ <%= 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') %> +
+
+ <%= 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') %> +
+ +
<%= submit_tag t('site.search.submit_text') %>
+ +
+
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index df86f2b9d..ef7efcbb9 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -10,6 +10,7 @@ Rails.application.config.assets.version = '1.0' # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. Rails.application.config.assets.precompile += %w( index.js browse.js welcome.js fixthemap.js ) Rails.application.config.assets.precompile += %w( user.js diary_entry.js ) +Rails.application.config.assets.precompile += %w( routing.js ) Rails.application.config.assets.precompile += %w( large-ltr.css small-ltr.css print-ltr.css ) Rails.application.config.assets.precompile += %w( large-rtl.css small-rtl.css print-rtl.css ) Rails.application.config.assets.precompile += %w( leaflet-all.css leaflet.ie.css ) diff --git a/config/locales/de.yml b/config/locales/de.yml index 406348f8a..1ca436b54 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1446,6 +1446,12 @@ de: where_am_i: Wo bin ich? where_am_i_title: Die momentane Position mit der Suchmaschine anzeigen submit_text: Los + 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" key: table: entry: @@ -2328,6 +2334,40 @@ de: comment: Kommentar 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_bicycle: "Fahrrad (GraphHopper)" + graphhopper_foot: "Fuss (GraphHopper)" + mapquest_bicycle: "Fahrrad (MapQuest)" + mapquest_foot: "Fuss (MapQuest)" + mapquest_car: "Auto (MapQuest)" + osrm_car: "Auto (OSRM)" + distance: "Distanz:" + time: "Zeit:" + errors: + no_route: "Wir konnten keine Strecke zwischen diesen beiden Orten berechnen." + no_place: "Wir konnten den Ort nicht finden." + 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)" + courtesy: "Fahranweisungen stammen von %{link}" query: node: Knoten way: Weg diff --git a/config/locales/en.yml b/config/locales/en.yml index 9424b7fde..88d42282c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1378,6 +1378,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" @@ -2172,6 +2178,40 @@ 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)" + courtesy: "Directions courtesy of %{link}" + time: "Time" query: node: Node way: Way diff --git a/config/routes.rb b/config/routes.rb index 7084d1c8d..3d47c6dcf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -247,6 +247,9 @@ OpenStreetMap::Application.routes.draw do match '/geocoder/search_osm_nominatim_reverse' => 'geocoder#search_osm_nominatim_reverse', :via => :get match '/geocoder/search_geonames_reverse' => 'geocoder#search_geonames_reverse', :via => :get + # directions + match '/directions' => 'directions#search', :via => :get, :as => :directions + # export match '/export/finish' => 'export#finish', :via => :post match '/export/embed' => 'export#embed', :via => :get diff --git a/vendor/assets/leaflet/leaflet.polyline.js b/vendor/assets/leaflet/leaflet.polyline.js new file mode 100755 index 000000000..b7e85d6c5 --- /dev/null +++ b/vendor/assets/leaflet/leaflet.polyline.js @@ -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