]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/leaflet.share.js
Merge remote-tracking branch 'upstream/pull/6249'
[rails.git] / app / assets / javascripts / leaflet.share.js
1 L.OSM.share = function (options) {
2   const control = L.OSM.sidebarPane(options, "share", "javascripts.share.title", "javascripts.share.title"),
3         marker = L.marker([0, 0], { draggable: true, icon: OSM.getMarker({ color: "var(--marker-blue)" }) }),
4         locationFilter = new L.LocationFilter({
5           enableButton: false,
6           adjustButton: false
7         });
8
9   function init(map, $ui) {
10     // Link / Embed
11
12     $ui.find("#link_marker").on("change", toggleMarker);
13
14     $ui.find(".btn-group .btn")
15       .on("shown.bs.tab", () => {
16         $ui.find(".tab-pane.active [id]")
17           .trigger("select");
18       });
19
20     $ui.find(".share-tab [id]").on("click", select);
21
22     // Image
23
24     $ui.find("#mapnik_scale").on("change", update);
25
26     $ui.find("#image_filter").bind("change", toggleFilter);
27
28     const csrfInput = $ui.find("#csrf_export")[0];
29     [[csrfInput.name, csrfInput.value]] = Object.entries(OSM.csrf);
30
31     function downloadBlob(blob, filename) {
32       const url = URL.createObjectURL(blob);
33       const a = document.createElement("a");
34       a.href = url;
35       a.download = filename;
36       document.body.appendChild(a);
37       a.click();
38       document.body.removeChild(a);
39       URL.revokeObjectURL(url);
40     }
41
42     async function handleExportSuccess(fetchResponse) {
43       try {
44         const blob = await fetchResponse.response.blob();
45         const filename = OSM.i18n.t("javascripts.share.filename");
46         downloadBlob(blob, filename);
47       } catch (err) {
48         // eslint-disable-next-line no-alert
49         alert(OSM.i18n.t("javascripts.share.export_failed", { reason: "(blob error)" }));
50       }
51     }
52
53     async function handleExportError(event) {
54       let detailMessage;
55       try {
56         detailMessage = event?.detail?.error?.message;
57         if (!detailMessage) {
58           const responseText = await event.detail.fetchResponse.responseText;
59           const parser = new DOMParser();
60           const doc = parser.parseFromString(responseText, "text/html");
61           detailMessage = doc.body ? doc.body.textContent.trim() : "(unknown)";
62         }
63       } catch (err) {
64         detailMessage = "(unknown)";
65       }
66       // eslint-disable-next-line no-alert
67       alert(OSM.i18n.t("javascripts.share.export_failed", { reason: detailMessage }));
68     }
69
70     document.getElementById("export-image").addEventListener("turbo:submit-end", function (event) {
71       if (event.detail.success) {
72         handleExportSuccess(event.detail.fetchResponse);
73       } else {
74         handleExportError(event);
75       }
76     });
77
78     document.getElementById("export-image").addEventListener("turbo:before-fetch-response", function (event) {
79       const response = event.detail.fetchResponse.response;
80       const contentType = response.headers.get("content-type");
81
82       if (!response.ok && contentType?.includes("text/html")) {
83         // Prevent Turbo from replacing the current page with an error HTML response
84         // from the image export endpoint
85         event.preventDefault();
86         event.stopPropagation();
87       }
88     });
89
90     locationFilter
91       .on("change", update)
92       .addTo(map);
93
94     marker.on("dragend", movedMarker);
95     map.on("move", movedMap);
96     map.on("moveend baselayerchange overlayadd overlayremove", update);
97
98     $ui
99       .on("show", shown)
100       .on("hide", hidden);
101
102     update();
103
104     function shown() {
105       $("#mapnik_scale").val(getScale());
106       update();
107     }
108
109     function hidden() {
110       map.removeLayer(marker);
111       map.options.scrollWheelZoom = map.options.doubleClickZoom = true;
112       locationFilter.disable();
113       update();
114     }
115
116     function toggleMarker() {
117       if ($(this).is(":checked")) {
118         marker.setLatLng(map.getCenter());
119         map.addLayer(marker);
120         map.options.scrollWheelZoom = map.options.doubleClickZoom = "center";
121       } else {
122         map.removeLayer(marker);
123         map.options.scrollWheelZoom = map.options.doubleClickZoom = true;
124       }
125       update();
126     }
127
128     function toggleFilter() {
129       if ($(this).is(":checked")) {
130         locationFilter.setBounds(map.getBounds().pad(-0.2));
131         locationFilter.enable();
132       } else {
133         locationFilter.disable();
134       }
135       update();
136     }
137
138     function movedMap() {
139       marker.setLatLng(map.getCenter());
140       update();
141     }
142
143     function movedMarker() {
144       if (map.hasLayer(marker)) {
145         map.off("move", movedMap);
146         map.on("moveend", updateOnce);
147         map.panTo(marker.getLatLng());
148       }
149     }
150
151     function updateOnce() {
152       map.off("moveend", updateOnce);
153       map.on("move", movedMap);
154       update();
155     }
156
157     function escapeHTML(string) {
158       const htmlEscapes = {
159         "&": "&",
160         "<": "&lt;",
161         ">": "&gt;",
162         "\"": "&quot;",
163         "'": "&#x27;"
164       };
165       return string === null ? "" : String(string).replace(/[&<>"']/g, function (match) {
166         return htmlEscapes[match];
167       });
168     }
169
170     function update() {
171       const layer = map.getMapBaseLayer();
172       const canEmbed = Boolean(layer && layer.options.canEmbed);
173       let bounds = map.getBounds();
174
175       $("#link_marker")
176         .prop("checked", map.hasLayer(marker));
177
178       $("#image_filter")
179         .prop("checked", locationFilter.isEnabled());
180
181       // Link / Embed
182
183       $("#short_input").val(map.getShortUrl(marker));
184       $("#long_input").val(map.getUrl(marker));
185       $("#short_link").attr("href", map.getShortUrl(marker));
186       $("#long_link").attr("href", map.getUrl(marker));
187
188       const params = new URLSearchParams({
189         bbox: bounds.toBBoxString(),
190         layer: map.getMapBaseLayerId()
191       });
192
193       if (map.hasLayer(marker)) {
194         const latLng = marker.getLatLng().wrap();
195         params.set("marker", latLng.lat + "," + latLng.lng);
196       }
197
198       if (!canEmbed && $("#nav-embed").hasClass("active")) {
199         bootstrap.Tab.getOrCreateInstance($("#long_link")).show();
200       }
201       $("#embed_link")
202         .toggleClass("disabled", !canEmbed)
203         .parent()
204         .tooltip(canEmbed ? "disable" : "enable");
205
206       $("#embed_html").val(
207         "<iframe width=\"425\" height=\"350\" src=\"" +
208           escapeHTML(OSM.SERVER_PROTOCOL + "://" + OSM.SERVER_URL + "/export/embed.html?" + params) +
209           "\" style=\"border: 1px solid black\"></iframe><br/>" +
210           "<small><a href=\"" + escapeHTML(map.getUrl(marker)) + "\">" +
211           escapeHTML(OSM.i18n.t("javascripts.share.view_larger_map")) + "</a></small>");
212
213       // Geo URI
214
215       $("#geo_uri")
216         .attr("href", map.getGeoUri(marker))
217         .text(map.getGeoUri(marker));
218
219       // Image
220
221       if (locationFilter.isEnabled()) {
222         bounds = locationFilter.getBounds();
223       }
224
225       let scale = $("#mapnik_scale").val();
226       const size = L.bounds(L.CRS.EPSG3857.project(bounds.getSouthWest()),
227                             L.CRS.EPSG3857.project(bounds.getNorthEast())).getSize(),
228             maxScale = Math.floor(Math.sqrt(size.x * size.y / 0.3136));
229
230       $("#mapnik_minlon").val(bounds.getWest());
231       $("#mapnik_minlat").val(bounds.getSouth());
232       $("#mapnik_maxlon").val(bounds.getEast());
233       $("#mapnik_maxlat").val(bounds.getNorth());
234
235       if (scale < maxScale) {
236         scale = roundScale(maxScale);
237         $("#mapnik_scale").val(scale);
238       }
239
240       const mapWidth = Math.round(size.x / scale / 0.00028);
241       const mapHeight = Math.round(size.y / scale / 0.00028);
242       $("#mapnik_image_width").text(mapWidth);
243       $("#mapnik_image_height").text(mapHeight);
244
245       const canDownloadImage = Boolean(layer && layer.options.canDownloadImage);
246
247       $("#mapnik_image_layer").text(canDownloadImage ? layer.options.name : "");
248       $("#map_format").val(canDownloadImage ? layer.options.layerId : "");
249
250       $("#map_zoom").val(map.getZoom());
251       $("#mapnik_lon").val(map.getCenter().lng);
252       $("#mapnik_lat").val(map.getCenter().lat);
253       $("#map_width").val(mapWidth);
254       $("#map_height").val(mapHeight);
255
256       $("#export-image").toggle(canDownloadImage);
257       $("#export-warning").toggle(!canDownloadImage);
258       $("#mapnik_scale_row").toggle(canDownloadImage && layer.options.layerId === "mapnik");
259     }
260
261     function select() {
262       $(this).trigger("select");
263     }
264
265     function getScale() {
266       const bounds = map.getBounds(),
267             centerLat = bounds.getCenter().lat,
268             halfWorldMeters = 6378137 * Math.PI * Math.cos(centerLat * Math.PI / 180),
269             meters = halfWorldMeters * (bounds.getEast() - bounds.getWest()) / 180,
270             pixelsPerMeter = map.getSize().x / meters,
271             metersPerPixel = 1 / (92 * 39.3701);
272       return Math.round(1 / (pixelsPerMeter * metersPerPixel));
273     }
274
275     function roundScale(scale) {
276       const precision = 5 * Math.pow(10, Math.floor(Math.LOG10E * Math.log(scale)) - 2);
277       return precision * Math.ceil(scale / precision);
278     }
279   }
280
281   control.onAddPane = function (map, button, $ui) {
282     $("#content").addClass("overlay-right-sidebar");
283
284     control.onContentLoaded = () => init(map, $ui);
285     $ui.one("show", control.loadContent);
286   };
287
288   return control;
289 };