jsrouting: mouseover an instruction, highlight the route segment
[rails.git] / app / assets / javascripts / routing.js.erb
1 /*
2     osm.org routing interface
3 */
4
5 var TURN_INSTRUCTIONS=[]
6
7 var ROUTING_POLYLINE={
8     color: '#03f',
9     opacity: 0.3,
10     weight: 10
11 };
12
13 var ROUTING_POLYLINE_HIGHLIGHT={
14     color: '#ff0',
15     opacity: 0.5,
16     weight: 12
17 };
18
19
20 OSM.RoutingEngines={
21     list: []
22 };
23
24 OSM.Routing=function(map,name,jqSearch) {
25     var r={};
26
27     TURN_INSTRUCTIONS=["",
28     I18n.t('javascripts.directions.instructions.continue_on'),      // 1
29     I18n.t('javascripts.directions.instructions.slight_right'),     // 2
30     I18n.t('javascripts.directions.instructions.turn_right'),       // 3
31     I18n.t('javascripts.directions.instructions.sharp_right'),      // 4
32     I18n.t('javascripts.directions.instructions.uturn'),            // 5
33     I18n.t('javascripts.directions.instructions.sharp_left'),       // 6
34     I18n.t('javascripts.directions.instructions.turn_left'),        // 7
35     I18n.t('javascripts.directions.instructions.slight_left'),      // 8
36     I18n.t('javascripts.directions.instructions.via_point'),        // 9
37     I18n.t('javascripts.directions.instructions.follow'),           // 10
38     I18n.t('javascripts.directions.instructions.roundabout'),       // 11
39     I18n.t('javascripts.directions.instructions.leave_roundabout'), // 12
40     I18n.t('javascripts.directions.instructions.stay_roundabout'),  // 13
41     I18n.t('javascripts.directions.instructions.start'),            // 14
42     I18n.t('javascripts.directions.instructions.destination'),      // 15
43     I18n.t('javascripts.directions.instructions.against_oneway'),   // 16
44     I18n.t('javascripts.directions.instructions.end_oneway')]       // 17
45
46     r.map=map;              // Leaflet map
47     r.name=name;            // global variable name of this instance (needed for JSONP)
48     r.jqSearch=jqSearch;    // JQuery object for search panel
49
50     r.route_from=null;      // null=unset, false=awaiting response, [lat,lon]=geocoded
51     r.route_to=null;        //  |
52     r.awaitingGeocode=false;// true if the user has requested a route, but we're waiting on a geocode result
53     r.awaitingRoute=false;  // true if we've asked the engine for a route and are waiting to hear back
54     r.dragging=false;       // true if the user is dragging a start/end point
55     r.viaPoints=[];         // not yet used
56
57     r.polyline=null;        // Leaflet polyline object
58     r.popup=null;           // Leaflet popup object
59     r.marker_from=null;     // Leaflet from marker
60     r.marker_to=null;       // Leaflet to marker
61
62     r.chosenEngine=null;    // currently selected routing engine
63
64     var icon_from = L.icon({
65         iconUrl: <%= asset_path('marker-green.png').to_json %>,
66         iconSize: [25, 41],
67         iconAnchor: [12, 41],
68         popupAnchor: [1, -34],
69         shadowUrl: <%= asset_path('images/marker-shadow.png').to_json %>,
70         shadowSize: [41, 41]
71     });
72     var icon_to = L.icon({
73         iconUrl: <%= asset_path('marker-red.png').to_json %>,
74         iconSize: [25, 41],
75         iconAnchor: [12, 41],
76         popupAnchor: [1, -34],
77         shadowUrl: <%= asset_path('images/marker-shadow.png').to_json %>,
78         shadowSize: [41, 41]
79     });
80
81     // Geocoding
82
83     r.geocode=function(id,event) { var _this=this;
84         var field=event.target;
85         var v=event.target.value;
86         var querystring = '<%= NOMINATIM_URL %>search?q=' + encodeURIComponent(v) + '&format=json';
87         // *** &accept-language=<%#= request.user_preferred_languages.join(',') %>
88         r[field.id]=false;
89         $.getJSON(querystring, function(json) { _this._gotGeocode(json,field); });
90     };
91     
92     r._gotGeocode=function(json,field) {
93         if (json.length==0) {
94             alert(I18n.t('javascripts.directions.errors.no_place'));
95             r[field.id]=null;
96             return;
97         }
98         field.value=json[0].display_name;
99         var lat=Number(json[0].lat), lon=Number(json[0].lon);
100         r[field.id]=[lat,lon];
101         r.updateMarker(field.id);
102         if (r.awaitingGeocode) {
103             r.awaitingGeocode=false;
104             r.requestRoute(true, true);
105         }
106     };
107
108     // Drag and drop markers
109     
110     r.handleDrop=function(e) {
111         var oe=e.originalEvent;
112         var id=oe.dataTransfer.getData('id');
113         var pt=L.DomEvent.getMousePosition(oe,map.getContainer());  // co-ordinates of the mouse pointer at present
114         pt.x+=Number(oe.dataTransfer.getData('offsetX'));
115         pt.y+=Number(oe.dataTransfer.getData('offsetY'));
116         var ll=map.containerPointToLatLng(pt);
117         r.createMarker(ll,id);
118         r.setNumericInput(ll,id);
119         r.requestRoute(true, false);
120         // update to/from field
121     };
122     r.createMarker=function(latlng,id) {
123         if (r[id]) r.map.removeLayer(r[id]);
124         r[id]=L.marker(latlng, {
125             icon: id=='marker_from' ? icon_from : icon_to,
126             draggable: true,
127             name: id
128         }).addTo(r.map);
129         r[id].on('drag',r.markerDragged);
130         r[id].on('dragend',r.markerDragged);
131     };
132     // Update marker from geocoded route input
133     r.updateMarker=function(id) {
134         var m=id.replace('route','marker');
135         if (!r[m]) { r.createMarker(r[id],m); return; }
136         var ll=r[m].getLatLng();
137         if (ll.lat!=r[id][0] || ll.lng!=r[id][1]) {
138             r.createMarker(r[id],m);
139         }
140     };
141     // Marker has been dragged
142     r.markerDragged=function(e) {
143         r.dragging=(e.type=='drag');    // true for drag, false for dragend
144         if (r.dragging && !r.chosenEngine.draggable) return;
145         if (r.dragging && r.awaitingRoute) return;
146         r.setNumericInput(e.target.getLatLng(), e.target.options.name);
147         r.requestRoute(!r.dragging, false);
148     };
149     // Set a route input field to a numeric value
150     r.setNumericInput=function(ll,id) {
151         var routeid=id.replace('marker','route');
152         r[routeid]=[ll.lat,ll.lng];
153         $("[name="+routeid+"]:visible").val(Math.round(ll.lat*10000)/10000+" "+Math.round(ll.lng*10000)/10000);
154     }
155     
156     // Route-fetching UI
157
158     r.requestRoute=function(isFinal, updateZoom) {
159         if (r.route_from && r.route_to) {
160             $(".query_wrapper.routing .spinner").show();
161             r.awaitingRoute=true;
162             r.chosenEngine.getRoute(isFinal,[r.route_from,r.route_to]);
163             if(updateZoom){
164                 r.map.fitBounds(L.latLngBounds([r.route_from, r.route_to]).pad(0.05));
165             }
166             // then, when the route has been fetched, it'll call the engine's gotRoute function
167         } else if (r.route_from==false || r.route_to==false) {
168             // we're waiting for a Nominatim response before we can request a route
169             r.awaitingGeocode=true;
170         }
171     };
172
173     // Take an array of Leaflet LatLngs and draw it as a polyline
174     r.setPolyline=function(line) {
175         if (r.polyline) map.removeLayer(r.polyline);
176         r.polyline=L.polyline(line, ROUTING_POLYLINE).addTo(r.map);
177     };
178
179     // Take directions and write them out
180     // data = { steps: array of [latlng, sprite number, instruction text, distance in metres, highlightPolyline] }
181     // sprite numbers equate to OSRM's route_instructions turn values
182     r.setItinerary=function(data) {
183         // Create base table
184         $("#content").removeClass("overlay-sidebar");
185         $('#sidebar_content').empty();
186         var html=('<h2><a class="geolink" href="#" onclick="$(~.close_directions~).click();return false;">' +
187                   '<span class="icon close"></span></a>' + I18n.t('javascripts.directions.directions') + 
188                   '</h2><p id="routing_summary">' + 
189                   I18n.t('javascripts.directions.distance') + ': ' + r.formatDistance(data.distance)+ '. ' +
190                   I18n.t('javascripts.directions.time'    ) + ': ' + r.formatTime(data.time) + '.</p>' +
191                   '<table id="turnbyturn" />').replace(/~/g,"'");
192         $('#sidebar_content').html(html);
193         // Add each row
194         var cumulative=0;
195         for (var i=0; i<data.steps.length; i++) {
196             var step=data.steps[i];
197             // Distance
198             var dist=step[3];
199             if (dist<5) { dist=""; }
200             else if (dist<200) { dist=Math.round(dist/10)*10+"m"; }
201             else if (dist<1500) { dist=Math.round(dist/100)*100+"m"; }
202             else if (dist<5000) { dist=Math.round(dist/100)/10+"km"; }
203             else { dist=Math.round(dist/1000)+"km"; }
204             // Add to table
205             var row=$("<tr class='turn'/>");
206             row.append("<td class='direction i"+step[1]+"'> ");
207             row.append("<td class='instruction'>"+step[2]);
208             row.append("<td class='distance'>"+dist);
209             with ({ num: i, ll: step[0], lineseg: step[4] }) {
210                 row.on('click',function(e) { r.clickTurn(num, ll); });
211                 row.hover(function(e){r.highlightSegment(lineseg);}, function(e){r.unhighlightSegment();});
212             };
213             $('#turnbyturn').append(row);
214             cumulative+=step[3];
215         }
216         $('#sidebar_content').append('<p id="routing_credit">' + r.chosenEngine.creditline + '</p>');
217
218     };
219     r.clickTurn=function(num,latlng) {
220         r.popup=L.popup().setLatLng(latlng).setContent("<p>"+(num+1)+"</p>").openOn(r.map);
221     };
222     r.highlightSegment=function(lineseg){
223         if (r.highlighted) map.removeLayer(r.highlighted);
224         r.highlighted=L.polyline(lineseg, ROUTING_POLYLINE_HIGHLIGHT).addTo(r.map);
225     }
226     r.unhighlightSegment=function(){
227         if (r.highlighted) map.removeLayer(r.highlighted);
228     }
229     r.formatDistance=function(m) {
230         if      (m < 1000 ) { return Math.round(m) + "m"; }
231         else if (m < 10000) { return (m/1000.0).toFixed(1) + "km"; }
232         else                { return Math.round(m / 1000)  + "km"; }
233     };
234     r.formatTime=function(s) {
235         var d=new Date(s*1000); var h=d.getHours(); var m=d.getMinutes();
236         return h+":"+(m<10 ? '0' : '')+m;
237     };
238
239     // Close all routing UI
240     
241     r.close=function() {
242         $("#content").addClass("overlay-sidebar");
243         r.route_from=r.route_to=null;
244         $(".query_wrapper.routing input").val("");
245         var remove=['polyline','popup','marker_from','marker_to'];
246         for (var i=0; i<remove.length; i++) {
247             if (r[remove[i]]) { map.removeLayer(r[remove[i]]); r[remove[i]]=null; }
248         }
249     };
250
251     // Routing engine handling
252
253     // Add all engines
254     var list=OSM.RoutingEngines.list;
255     list.sort(function(a,b) { return I18n.t(a.name)>I18n.t(b.name); });
256     var select=r.jqSearch.find('select.routing_engines');
257     for (var i=0; i<list.length; i++) {
258         // Set up JSONP callback
259         with ({num: i}) {
260             list[num].requestJSONP=function(url) {
261                 var script = document.createElement('script');
262                 script.src = url+r.name+".gotRoute"+num;
263                 document.body.appendChild(script); 
264             };
265             list[num].requestCORS=function(url) {
266                 $.ajax({ url: url, method: "GET", data: {}, dataType: 'json', success: r['gotRoute'+num] });
267             };
268             r['gotRoute'+num]=function(data) { 
269                 r.awaitingRoute=false;
270                 $(".query_wrapper.routing .spinner").hide();
271                 if (!list[num].gotRoute(r,data)) {
272                     // No route found
273                     if (r.polyline) {
274                         map.removeLayer(r.polyline);
275                         r.polyline=null;
276                     }
277                     if (!r.dragging) { alert(I18n.t('javascripts.directions.errors.no_route')); }
278                 }
279             };
280         }
281         select.append("<option value='"+i+"'>"+I18n.t(list[i].name)+"</option>");
282     }
283     r.engines=list;
284     r.chosenEngine=list[0]; // default to first engine
285
286     // Choose an engine on dropdown change
287     r.selectEngine=function(e) {
288         r.chosenEngine=r.engines[e.target.selectedIndex];
289         if (r.polyline){ // and if a route is currently showing, must also refresh, else confusion
290             r.requestRoute(true, false);
291         }
292     };
293     // Choose an engine by name
294     r.chooseEngine=function(name) {
295         for (var i=0; i<r.engines.length; i++) {
296             if (r.engines[i].name==name) {
297                 r.chosenEngine=r.engines[i];
298                 r.jqSearch.find('select.routing_engines').val(i);
299             }
300         }
301     };
302
303     return r;
304 };