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