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