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