]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index/directions.js
Make reverse geocoding request with zoom using 'Where is this?'
[rails.git] / app / assets / javascripts / index / directions.js
1 //= require ./directions-endpoint
2 //= require_self
3 //= require_tree ./directions
4
5 OSM.Directions = function (map) {
6   let controller = null; // the AbortController for the current route request if a route request is in progress
7   let lastLocation = [];
8   let chosenEngine;
9
10   const popup = L.popup({ autoPanPadding: [100, 100] });
11
12   const polyline = L.polyline([], {
13     color: "#03f",
14     opacity: 0.3,
15     weight: 10
16   });
17
18   const highlight = L.polyline([], {
19     color: "#ff0",
20     opacity: 0.5,
21     weight: 12
22   });
23
24   const endpointDragCallback = function (dragging) {
25     if (!map.hasLayer(polyline)) return;
26     if (dragging && !chosenEngine.draggable) return;
27     if (dragging && controller) return;
28
29     getRoute(false, !dragging);
30   };
31   const endpointChangeCallback = function () {
32     getRoute(true, true);
33   };
34
35   const endpoints = [
36     OSM.DirectionsEndpoint(map, $("input[name='route_from']"), { icon: "MARKER_GREEN" }, endpointDragCallback, endpointChangeCallback),
37     OSM.DirectionsEndpoint(map, $("input[name='route_to']"), { icon: "MARKER_RED" }, endpointDragCallback, endpointChangeCallback)
38   ];
39
40   let downloadURL = null;
41
42   const expiry = new Date();
43   expiry.setYear(expiry.getFullYear() + 10);
44
45   const modeGroup = $(".routing_modes");
46   const select = $("select.routing_engines");
47
48   $(".directions_form .reverse_directions").on("click", function () {
49     const coordFrom = endpoints[0].latlng,
50           coordTo = endpoints[1].latlng;
51     let routeFrom = "",
52         routeTo = "";
53     if (coordFrom) {
54       routeFrom = coordFrom.lat + "," + coordFrom.lng;
55     }
56     if (coordTo) {
57       routeTo = coordTo.lat + "," + coordTo.lng;
58     }
59     endpoints[0].swapCachedReverseGeocodes(endpoints[1]);
60
61     OSM.router.route("/directions?" + new URLSearchParams({
62       route: routeTo + ";" + routeFrom
63     }));
64   });
65
66   $(".directions_form .btn-close").on("click", function (e) {
67     e.preventDefault();
68     $(".describe_location").toggle(!endpoints[1].value);
69     $(".search_form input[name='query']").val(endpoints[1].value);
70     OSM.router.route("/" + OSM.formatHash(map));
71   });
72
73   function formatTotalDistance(m) {
74     if (m < 1000) {
75       return OSM.i18n.t("javascripts.directions.distance_m", { distance: Math.round(m) });
76     } else if (m < 10000) {
77       return OSM.i18n.t("javascripts.directions.distance_km", { distance: (m / 1000.0).toFixed(1) });
78     } else {
79       return OSM.i18n.t("javascripts.directions.distance_km", { distance: Math.round(m / 1000) });
80     }
81   }
82
83   function formatStepDistance(m) {
84     if (m < 5) {
85       return "";
86     } else if (m < 200) {
87       return OSM.i18n.t("javascripts.directions.distance_m", { distance: String(Math.round(m / 10) * 10) });
88     } else if (m < 1500) {
89       return OSM.i18n.t("javascripts.directions.distance_m", { distance: String(Math.round(m / 100) * 100) });
90     } else if (m < 5000) {
91       return OSM.i18n.t("javascripts.directions.distance_km", { distance: String(Math.round(m / 100) / 10) });
92     } else {
93       return OSM.i18n.t("javascripts.directions.distance_km", { distance: String(Math.round(m / 1000)) });
94     }
95   }
96
97   function formatHeight(m) {
98     return OSM.i18n.t("javascripts.directions.distance_m", { distance: Math.round(m) });
99   }
100
101   function formatTime(s) {
102     let m = Math.round(s / 60);
103     const h = Math.floor(m / 60);
104     m -= h * 60;
105     return h + ":" + (m < 10 ? "0" : "") + m;
106   }
107
108   function setEngine(id) {
109     const engines = OSM.Directions.engines;
110     const desired = engines.find(engine => engine.id === id);
111     if (!desired || (chosenEngine && chosenEngine.id === id)) return;
112     chosenEngine = desired;
113
114     const modes = engines
115       .filter(engine => engine.provider === chosenEngine.provider)
116       .map(engine => engine.mode);
117     modeGroup
118       .find("input[id]")
119       .prop("disabled", function () {
120         return !modes.includes(this.id);
121       })
122       .prop("checked", function () {
123         return this.id === chosenEngine.mode;
124       });
125
126     const providers = engines
127       .filter(engine => engine.mode === chosenEngine.mode)
128       .map(engine => engine.provider);
129     select
130       .find("option[value]")
131       .prop("disabled", function () {
132         return !providers.includes(this.value);
133       });
134     select.val(chosenEngine.provider);
135   }
136
137   function getRoute(fitRoute, reportErrors) {
138     // Cancel any route that is already in progress
139     if (controller) controller.abort();
140
141     const points = endpoints.map(p => p.latlng);
142
143     if (!points[0] || !points[1]) return;
144     $("header").addClass("closed");
145
146     OSM.router.replace("/directions?" + new URLSearchParams({
147       engine: chosenEngine.id,
148       route: points.map(p => `${p.lat},${p.lng}`).join(";")
149     }));
150
151     // copy loading item to sidebar and display it. we copy it, rather than
152     // just using it in-place and replacing it in case it has to be used
153     // again.
154     $("#directions_content").html($(".directions_form .loader_copy").html());
155     map.setSidebarOverlaid(false);
156     controller = new AbortController();
157     chosenEngine.getRoute(points, controller.signal).then(function (route) {
158       polyline
159         .setLatLngs(route.line)
160         .addTo(map);
161
162       if (fitRoute) {
163         map.fitBounds(polyline.getBounds().pad(0.05));
164       }
165
166       const distanceText = $("<p>").append(
167         OSM.i18n.t("javascripts.directions.distance") + ": " + formatTotalDistance(route.distance) + ". " +
168         OSM.i18n.t("javascripts.directions.time") + ": " + formatTime(route.time) + ".");
169       if (typeof route.ascend !== "undefined" && typeof route.descend !== "undefined") {
170         distanceText.append(
171           $("<br>"),
172           OSM.i18n.t("javascripts.directions.ascend") + ": " + formatHeight(route.ascend) + ". " +
173           OSM.i18n.t("javascripts.directions.descend") + ": " + formatHeight(route.descend) + ".");
174       }
175
176       const turnByTurnTable = $("<table class='table table-hover table-sm mb-3'>")
177         .append($("<tbody>"));
178
179       $("#directions_content")
180         .empty()
181         .append(
182           distanceText,
183           turnByTurnTable
184         );
185
186       // Add each row
187       turnByTurnTable.append(route.steps.map(([direction, instruction, dist, lineseg], i) => {
188         const row = $("<tr class='turn'/>");
189         if (direction) {
190           row.append("<td class='border-0'><svg width='20' height='20' class='d-block'><use href='#routing-sprite-" + direction + "' /></svg></td>");
191         } else {
192           row.append("<td class='border-0'>");
193         }
194         row.append(`<td><b>${i + 1}.</b> ${instruction}`);
195         row.append("<td class='distance text-body-secondary text-end'>" + formatStepDistance(dist));
196
197         row.on("click", function () {
198           popup
199             .setLatLng(lineseg[0])
200             .setContent(`<p><b>${i + 1}.</b> ${instruction}</p>`)
201             .openOn(map);
202         });
203
204         row.hover(function () {
205           highlight
206             .setLatLngs(lineseg)
207             .addTo(map);
208         }, function () {
209           map.removeLayer(highlight);
210         });
211
212         return row;
213       }));
214
215       const blob = new Blob([JSON.stringify(polyline.toGeoJSON())], { type: "application/json" });
216       URL.revokeObjectURL(downloadURL);
217       downloadURL = URL.createObjectURL(blob);
218
219       $("#directions_content").append(`<p class="text-center"><a href="${downloadURL}" download="${
220         OSM.i18n.t("javascripts.directions.filename")
221       }">${
222         OSM.i18n.t("javascripts.directions.download")
223       }</a></p>`);
224
225       $("#directions_content").append("<p class=\"text-center\">" +
226         OSM.i18n.t("javascripts.directions.instructions.courtesy", { link: chosenEngine.creditline }) +
227         "</p>");
228     }).catch(function () {
229       map.removeLayer(polyline);
230       if (reportErrors) {
231         $("#directions_content").html("<div class=\"alert alert-danger\">" + OSM.i18n.t("javascripts.directions.errors.no_route") + "</div>");
232       }
233     }).finally(function () {
234       controller = null;
235     });
236   }
237
238   function hideRoute(e) {
239     e.stopPropagation();
240     map.removeLayer(polyline);
241     $("#directions_content").html("");
242     popup.close();
243     map.setSidebarOverlaid(true);
244     // TODO: collapse width of sidebar back to previous
245   }
246
247   setEngine("fossgis_osrm_car");
248   setEngine(Cookies.get("_osm_directions_engine"));
249
250   modeGroup.on("change", "input[name='modes']", function (e) {
251     setEngine(chosenEngine.provider + "_" + e.target.id);
252     Cookies.set("_osm_directions_engine", chosenEngine.id, { secure: true, expires: expiry, path: "/", samesite: "lax" });
253     getRoute(true, true);
254   });
255
256   select.on("change", function (e) {
257     setEngine(e.target.value + "_" + chosenEngine.mode);
258     Cookies.set("_osm_directions_engine", chosenEngine.id, { secure: true, expires: expiry, path: "/", samesite: "lax" });
259     getRoute(true, true);
260   });
261
262   $(".directions_form").on("submit", function (e) {
263     e.preventDefault();
264     getRoute(true, true);
265   });
266
267   $(".routing_marker_column img").on("dragstart", function (e) {
268     const dt = e.originalEvent.dataTransfer;
269     dt.effectAllowed = "move";
270     const dragData = { type: $(this).data("type") };
271     dt.setData("text", JSON.stringify(dragData));
272     if (dt.setDragImage) {
273       const img = $("<img>").attr("src", $(e.originalEvent.target).attr("src"));
274       dt.setDragImage(img.get(0), 12, 21);
275     }
276   });
277
278   function sendstartinglocation({ latlng: { lat, lng } }) {
279     map.fire("startinglocation", { latlng: [lat, lng] });
280   }
281
282   function startingLocationListener({ latlng }) {
283     if (endpoints[0].value) return;
284     endpoints[0].setValue(latlng.join(", "));
285   }
286
287   map.on("locationfound", ({ latlng: { lat, lng } }) =>
288     lastLocation = [lat, lng]
289   ).on("locateactivate", () => {
290     map.once("startinglocation", startingLocationListener);
291   });
292
293   function initializeFromParams() {
294     const params = new URLSearchParams(location.search),
295           route = (params.get("route") || "").split(";");
296
297     if (params.has("engine")) setEngine(params.get("engine"));
298
299     endpoints[0].setValue(params.get("from") || route[0] || lastLocation.join(", "));
300     endpoints[1].setValue(params.get("to") || route[1] || "");
301   }
302
303   function enableListeners() {
304     $("#sidebar .sidebar-close-controls button").on("click", hideRoute);
305
306     $("#map").on("dragend dragover", function (e) {
307       e.preventDefault();
308     });
309
310     $("#map").on("drop", function (e) {
311       e.preventDefault();
312       const oe = e.originalEvent;
313       const dragData = JSON.parse(oe.dataTransfer.getData("text"));
314       const type = dragData.type;
315       const pt = L.DomEvent.getMousePosition(oe, map.getContainer()); // co-ordinates of the mouse pointer at present
316       pt.y += 20;
317       const ll = map.containerPointToLatLng(pt);
318       const llWithPrecision = OSM.cropLocation(ll, map.getZoom());
319       endpoints[type === "from" ? 0 : 1].setValue(llWithPrecision.join(", "));
320     });
321
322     map.on("locationfound", sendstartinglocation);
323
324     endpoints[0].enableListeners();
325     endpoints[1].enableListeners();
326   }
327
328   const page = {};
329
330   page.pushstate = page.popstate = function () {
331     if ($("#directions_content").length) {
332       page.load();
333     } else {
334       initializeFromParams();
335
336       $(".search_form").hide();
337       $(".directions_form").show();
338
339       OSM.loadSidebarContent("/directions", enableListeners);
340
341       map.setSidebarOverlaid(!endpoints[0].latlng || !endpoints[1].latlng);
342     }
343   };
344
345   page.load = function () {
346     initializeFromParams();
347
348     $(".search_form").hide();
349     $(".directions_form").show();
350
351     enableListeners();
352
353     map.setSidebarOverlaid(!endpoints[0].latlng || !endpoints[1].latlng);
354   };
355
356   page.unload = function () {
357     $(".search_form").show();
358     $(".directions_form").hide();
359
360     $("#sidebar .sidebar-close-controls button").off("click", hideRoute);
361     $("#map").off("dragend dragover drop");
362     map.off("locationfound", sendstartinglocation);
363
364     endpoints[0].disableListeners();
365     endpoints[1].disableListeners();
366
367     endpoints[0].clearValue();
368     endpoints[1].clearValue();
369
370     map
371       .removeLayer(popup)
372       .removeLayer(polyline);
373   };
374
375   return page;
376 };
377
378 OSM.Directions.engines = [];
379
380 OSM.Directions.addEngine = function (engine, supportsHTTPS) {
381   if (location.protocol === "http:" || supportsHTTPS) {
382     engine.id = engine.provider + "_" + engine.mode;
383     OSM.Directions.engines.push(engine);
384   }
385 };