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({
9 function init(map, $ui) {
12 $ui.find("#link_marker").on("change", toggleMarker);
14 $ui.find(".btn-group .btn")
15 .on("shown.bs.tab", () => {
16 $ui.find(".tab-pane.active [id]")
20 $ui.find(".share-tab [id]").on("click", select);
24 $ui.find("#mapnik_scale").on("change", update);
26 $ui.find("#image_filter").bind("change", toggleFilter);
28 const csrfInput = $ui.find("#csrf_export")[0];
29 [[csrfInput.name, csrfInput.value]] = Object.entries(OSM.csrf);
31 function downloadBlob(blob, filename) {
32 const url = URL.createObjectURL(blob);
33 const a = document.createElement("a");
35 a.download = filename;
36 document.body.appendChild(a);
38 document.body.removeChild(a);
39 URL.revokeObjectURL(url);
42 async function handleExportSuccess(fetchResponse) {
44 const blob = await fetchResponse.response.blob();
45 const filename = OSM.i18n.t("javascripts.share.filename");
46 downloadBlob(blob, filename);
48 // eslint-disable-next-line no-alert
49 alert(OSM.i18n.t("javascripts.share.export_failed", { reason: "(blob error)" }));
53 async function handleExportError(event) {
56 detailMessage = event?.detail?.error?.message;
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)";
64 detailMessage = "(unknown)";
66 // eslint-disable-next-line no-alert
67 alert(OSM.i18n.t("javascripts.share.export_failed", { reason: detailMessage }));
70 document.getElementById("export-image").addEventListener("turbo:submit-end", function (event) {
71 if (event.detail.success) {
72 handleExportSuccess(event.detail.fetchResponse);
74 handleExportError(event);
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");
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();
94 marker.on("dragend", movedMarker);
95 map.on("move", movedMap);
96 map.on("moveend baselayerchange overlayadd overlayremove", update);
105 $("#mapnik_scale").val(getScale());
110 map.removeLayer(marker);
111 map.options.scrollWheelZoom = map.options.doubleClickZoom = true;
112 locationFilter.disable();
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";
122 map.removeLayer(marker);
123 map.options.scrollWheelZoom = map.options.doubleClickZoom = true;
128 function toggleFilter() {
129 if ($(this).is(":checked")) {
130 locationFilter.setBounds(map.getBounds().pad(-0.2));
131 locationFilter.enable();
133 locationFilter.disable();
138 function movedMap() {
139 marker.setLatLng(map.getCenter());
143 function movedMarker() {
144 if (map.hasLayer(marker)) {
145 map.off("move", movedMap);
146 map.on("moveend", updateOnce);
147 map.panTo(marker.getLatLng());
151 function updateOnce() {
152 map.off("moveend", updateOnce);
153 map.on("move", movedMap);
157 function escapeHTML(string) {
158 const htmlEscapes = {
165 return string === null ? "" : String(string).replace(/[&<>"']/g, function (match) {
166 return htmlEscapes[match];
171 const layer = map.getMapBaseLayer();
172 const canEmbed = Boolean(layer && layer.options.canEmbed);
173 let bounds = map.getBounds();
176 .prop("checked", map.hasLayer(marker));
179 .prop("checked", locationFilter.isEnabled());
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));
188 const params = new URLSearchParams({
189 bbox: bounds.toBBoxString(),
190 layer: map.getMapBaseLayerId()
193 if (map.hasLayer(marker)) {
194 const latLng = marker.getLatLng().wrap();
195 params.set("marker", latLng.lat + "," + latLng.lng);
198 if (!canEmbed && $("#nav-embed").hasClass("active")) {
199 bootstrap.Tab.getOrCreateInstance($("#long_link")).show();
202 .toggleClass("disabled", !canEmbed)
204 .tooltip(canEmbed ? "disable" : "enable");
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>");
216 .attr("href", map.getGeoUri(marker))
217 .text(map.getGeoUri(marker));
221 if (locationFilter.isEnabled()) {
222 bounds = locationFilter.getBounds();
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));
230 $("#mapnik_minlon").val(bounds.getWest());
231 $("#mapnik_minlat").val(bounds.getSouth());
232 $("#mapnik_maxlon").val(bounds.getEast());
233 $("#mapnik_maxlat").val(bounds.getNorth());
235 if (scale < maxScale) {
236 scale = roundScale(maxScale);
237 $("#mapnik_scale").val(scale);
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);
245 const canDownloadImage = Boolean(layer && layer.options.canDownloadImage);
247 $("#mapnik_image_layer").text(canDownloadImage ? layer.options.name : "");
248 $("#map_format").val(canDownloadImage ? layer.options.layerId : "");
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);
256 $("#export-image").toggle(canDownloadImage);
257 $("#export-warning").toggle(!canDownloadImage);
258 $("#mapnik_scale_row").toggle(canDownloadImage && layer.options.layerId === "mapnik");
262 $(this).trigger("select");
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));
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);
281 control.onAddPane = function (map, button, $ui) {
282 $("#content").addClass("overlay-right-sidebar");
284 control.onContentLoaded = () => init(map, $ui);
285 $ui.one("show", control.loadContent);