1 //= require download_util
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({
11 function init(map, $ui) {
14 $ui.find("#link_marker").on("change", toggleMarker);
16 $ui.find(".btn-group .btn")
17 .on("shown.bs.tab", () => {
18 $ui.find(".tab-pane.active [id]")
22 $ui.find(".share-tab [id]").on("click", select);
26 $ui.find("#mapnik_scale").on("change", update);
28 $ui.find("#image_filter").bind("change", toggleFilter);
30 const csrfInput = $ui.find("#csrf_export")[0];
31 [[csrfInput.name, csrfInput.value]] = Object.entries(OSM.csrf);
33 document.getElementById("export-image")
34 .addEventListener("turbo:submit-end",
35 OSM.getTurboBlobHandler(OSM.i18n.t("javascripts.share.filename")));
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");
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();
53 marker.on("dragend", movedMarker);
54 map.on("move", movedMap);
55 map.on("moveend baselayerchange overlayadd overlayremove", update);
64 $("#mapnik_scale").val(getScale());
69 map.removeLayer(marker);
70 map.options.scrollWheelZoom = map.options.doubleClickZoom = true;
71 locationFilter.disable();
75 function toggleMarker() {
76 if ($(this).is(":checked")) {
77 marker.setLatLng(map.getCenter());
79 map.options.scrollWheelZoom = map.options.doubleClickZoom = "center";
81 map.removeLayer(marker);
82 map.options.scrollWheelZoom = map.options.doubleClickZoom = true;
87 function toggleFilter() {
88 if ($(this).is(":checked")) {
89 locationFilter.setBounds(map.getBounds().pad(-0.2));
90 locationFilter.enable();
92 locationFilter.disable();
98 marker.setLatLng(map.getCenter());
102 function movedMarker() {
103 if (map.hasLayer(marker)) {
104 map.off("move", movedMap);
105 map.on("moveend", updateOnce);
106 map.panTo(marker.getLatLng());
110 function updateOnce() {
111 map.off("moveend", updateOnce);
112 map.on("move", movedMap);
116 function escapeHTML(string) {
117 const htmlEscapes = {
124 return string === null ? "" : String(string).replace(/[&<>"']/g, function (match) {
125 return htmlEscapes[match];
130 const layer = map.getMapBaseLayer();
131 const canEmbed = Boolean(layer && layer.options.canEmbed);
132 let bounds = map.getBounds();
135 .prop("checked", map.hasLayer(marker));
138 .prop("checked", locationFilter.isEnabled());
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));
147 const params = new URLSearchParams({
148 bbox: bounds.toBBoxString(),
149 layer: map.getMapBaseLayerId()
152 if (map.hasLayer(marker)) {
153 const latLng = marker.getLatLng().wrap();
154 params.set("marker", latLng.lat + "," + latLng.lng);
157 if (!canEmbed && $("#nav-embed").hasClass("active")) {
158 bootstrap.Tab.getOrCreateInstance($("#long_link")).show();
161 .toggleClass("disabled", !canEmbed)
163 .tooltip(canEmbed ? "disable" : "enable");
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>");
175 .attr("href", map.getGeoUri(marker))
176 .text(map.getGeoUri(marker));
180 if (locationFilter.isEnabled()) {
181 bounds = locationFilter.getBounds();
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));
189 $("#mapnik_minlon").val(bounds.getWest());
190 $("#mapnik_minlat").val(bounds.getSouth());
191 $("#mapnik_maxlon").val(bounds.getEast());
192 $("#mapnik_maxlat").val(bounds.getNorth());
194 if (scale < maxScale) {
195 scale = roundScale(maxScale);
196 $("#mapnik_scale").val(scale);
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);
204 const canDownloadImage = Boolean(layer && layer.options.canDownloadImage);
206 $("#mapnik_image_layer").text(canDownloadImage ? layer.options.name : "");
207 $("#map_format").val(canDownloadImage ? layer.options.layerId : "");
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);
215 $("#export-image").toggle(canDownloadImage);
216 $("#export-warning").toggle(!canDownloadImage);
217 $("#mapnik_scale_row").toggle(canDownloadImage && layer.options.layerId === "mapnik");
221 $(this).trigger("select");
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));
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);
240 control.onAddPane = function (map, button, $ui) {
241 $("#content").addClass("overlay-right-sidebar");
243 control.onContentLoaded = () => init(map, $ui);
244 $ui.one("show", control.loadContent);