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