12

I am able to print the location of a given marker on the map using folium.plugins.MousePosition.
class GeoMap:
        
    def update(self, location_center:np.array, locations: np.array):
        self.map = folium.Map(location_center, zoom_start=10)
        for i in range(locations.shape[0]):
            location = tuple(locations[i, j] for j in range(locations.shape[1]))
            folium.Marker(
                location=location,
            ).add_to(self.map)
        formatter = "function(num) {return L.Util.formatNum(num, 3) + ' º ';};"
        plugins.MousePosition(
            position="topright",
            separator=" | ",
            empty_string="NaN",
            lng_first=True,
            num_digits=20,
            prefix="Coordinates:",
            lat_formatter=formatter,
            lng_formatter=formatter,
        ).add_to(self.map)
        
    def display(self):
        display(self.map)

But, I would like to enable the user to copy a marker's location on a folium Map by clicking on it. I suppose that there may be a way to get the location of a marker using a on_click event (in Python). But, I did not find any example on the web.

I am using Python, but if you have a solution that works well using Python and some Javascript it will be fine too.

Any help would be really appreciated !

Thanks,

Mistapopo
  • 433
  • 3
  • 16

2 Answers2

5

Copying with a button

Since you're working with markers, you can add a popup to each marker. The popup will open when you click on the marker icon. popup can take an html string as input so you can use this to insert a copy button or something similar.

Subsequently, you will need to add a javascript copy function to the folium html output. This can be done with MacroElement.

Implementing this would result in the following basic example:

import folium
import jinja2

location_center = [45.5236, -122.6750]
locations = [[45.5012, -122.6655],[45.5132, -122.6708],[45.5275, -122.6692],[45.5318, -122.6745]]

m = folium.Map(location_center, zoom_start=13)
for location in locations:
    folium.Marker(
        location=location,
        popup = f'<input type="text" value="{location[0]}, {location[1]}" id="myInput"><button onclick="myFunction()">Copy location</button>'
    ).add_to(m)
    
el = folium.MacroElement().add_to(m)
el._template = jinja2.Template("""
    {% macro script(this, kwargs) %}
    function myFunction() {
      /* Get the text field */
      var copyText = document.getElementById("myInput");

      /* Select the text field */
      copyText.select();
      copyText.setSelectionRange(0, 99999); /* For mobile devices */

      /* Copy the text inside the text field */
      document.execCommand("copy");
    }
    {% endmacro %}
""")

display(m)

enter image description here

Copying on click event

In case you wish to copy the latitude and longitude directly when clicking the marker: this is also possible, but requires monkey patching the Marker's jinja template to add a click event. Monkey patching is needed because the template for the markers is hard coded in folium.

Additionally, the function that will be triggered on the click can be defined with MacroElement:

import folium
import jinja2
from jinja2 import Template
from folium.map import Marker

tmpldata = """<!-- monkey patched Marker template -->
{% macro script(this, kwargs) %}
    var {{ this.get_name() }} = L.marker(
        {{ this.location|tojson }},
        {{ this.options|tojson }}
    ).addTo({{ this._parent.get_name() }}).on('click', onClick);
{% endmacro %}
"""

Marker._mytemplate = Template(tmpldata)
def myMarkerInit(self, *args, **kwargs):
    self.__init_orig__(*args, **kwargs)
    self._template = self._mytemplate
Marker.__init_orig__ = Marker.__init__
Marker.__init__ = myMarkerInit

location_center = [45.5236, -122.6750]
locations = [[45.5012, -122.6655],[45.5132, -122.6708],[45.5275, -122.6692],[45.5318, -122.6745]]

m = folium.Map(location_center, zoom_start=13)

for location in locations: #range(locations.shape[0]):
    folium.Marker(
        location=location,
        popup = f'<p id="latlon">{location[0]}, {location[1]}</p>'
    ).add_to(m)

el = folium.MacroElement().add_to(m)
el._template = jinja2.Template("""
    {% macro script(this, kwargs) %}
    function copy(text) {
        var input = document.createElement('textarea');
        input.innerHTML = text;
        document.body.appendChild(input);
        input.select();
        var result = document.execCommand('copy');
        document.body.removeChild(input);
        return result;
    };
    
    function getInnerText( sel ) {
        var txt = '';
        $( sel ).contents().each(function() {
            var children = $(this).children();
            txt += ' ' + this.nodeType === 3 ? this.nodeValue : children.length ? getInnerText( this ) : $(this).text();
        });
        return txt;
    };
    
    function onClick(e) {
       var popup = e.target.getPopup();
       var content = popup.getContent();
       text = getInnerText(content);
       copy(text);
    };
    {% endmacro %}
""")

display(m)

Copying from draggable markers

In case of working with draggable markers by setting draggable=True in the Marker object, copying the hardcoded coordinates from the popup makes no sense. In that case you'd better retrieve the latest coordinates from the Marker object and update the popup accordingly:

import folium
import jinja2
from jinja2 import Template
from folium.map import Marker

tmpldata = """<!-- monkey patched Marker template -->
{% macro script(this, kwargs) %}
    var {{ this.get_name() }} = L.marker(
        {{ this.location|tojson }},
        {{ this.options|tojson }}
    ).addTo({{ this._parent.get_name() }}).on('click', onClick);
{% endmacro %}
"""

Marker._mytemplate = Template(tmpldata)
def myMarkerInit(self, *args, **kwargs):
    self.__init_orig__(*args, **kwargs)
    self._template = self._mytemplate
Marker.__init_orig__ = Marker.__init__
Marker.__init__ = myMarkerInit

location_center = [45.5236, -122.6750]
locations = [[45.5012, -122.6655],[45.5132, -122.6708],[45.5275, -122.6692],[45.5318, -122.6745]]

m = folium.Map(location_center, zoom_start=13)

for location in locations: #range(locations.shape[0]):
    folium.Marker(
        location=location,
        popup = f'<p id="latlon">{location[0]}, {location[1]}</p>',
        draggable=True
    ).add_to(m)

el = folium.MacroElement().add_to(m)
el._template = jinja2.Template("""
    {% macro script(this, kwargs) %}
    function copy(text) {
        var input = document.createElement('textarea');
        input.innerHTML = text;
        document.body.appendChild(input);
        input.select();
        var result = document.execCommand('copy');
        document.body.removeChild(input);
        return result;
    };
    
    function onClick(e) {
       var lat = e.latlng.lat; 
       var lng = e.latlng.lng;
       var newContent = '<p id="latlon">' + lat + ', ' + lng + '</p>';
       e.target.setPopupContent(newContent);
       copy(lat + ', ' + lng);
    };
    {% endmacro %}
""")

display(m)
RJ Adriaansen
  • 9,131
  • 2
  • 12
  • 26
  • Do you know how to update the coordinates when you have `draggable=True`. And then dragging it, mine is having the same coordinates. If I would dragge mine from A to B. A would still be the coordinates that I can copy, but I want to copy B. – AnonymousUser Nov 05 '21 at 04:38
  • 1
    @AnonymousUser sure, I've updated the answer with an example – RJ Adriaansen Nov 05 '21 at 09:52
  • Is it possible to use those variables that are in the Javascript in python. For example I want to make a python variable with the value that `var lat` or `var lng` have. So I want `pythonLat = lat`, but lat is inside the JS so I can't use it in python. – AnonymousUser Nov 06 '21 at 07:02
  • @AnonymousUser Sure, but that's out of the scope of this question. You could add a [post request](https://stackoverflow.com/questions/29987323/how-do-i-send-data-from-js-to-python-with-flask) to a `flask` api endpoint in `onClick`, or use another solution like `dash_leaflet` as demonstrated in the other answer in this thread. – RJ Adriaansen Nov 06 '21 at 07:14
  • I get this error when I have your code **RecursionError at / maximum recursion depth exceeded in comparison** I get it when I reload the page, and I have added `sys.setrecursionlimit(50000)`, but instead of getting the error it instead stops running the server. – AnonymousUser Nov 09 '21 at 06:50
  • Restart the kernel and run again. – RJ Adriaansen Nov 09 '21 at 06:54
  • The one that works of the 3 different ones, are the first one **"Copying with a button"**. I think the reason that the other 2 doesn't work, is because you need to add `tmpldata ` and `Marker._mytemplate` – AnonymousUser Nov 09 '21 at 07:12
3

If you are not bound to using folium, this kind of interactivity can be achieve using dash-leaflet rather easily,

import json
import dash
import dash_leaflet as dl
import dash_html_components as html
from dash.dependencies import Input, Output, ALL

# Setup markers.
locations = [[45.5012, -122.6655], [45.5132, -122.6708], [45.5275, -122.6692], [45.5318, -122.6745]]
markers = [dl.Marker(position=l, id=dict(id=i)) for i, l in enumerate(locations)]
# Create small example dash app showing a map and a div (for logging).
app = dash.Dash(prevent_initial_callbacks=True)
app.layout = html.Div([
    dl.Map([dl.TileLayer()] + markers, center=[45.5236, -122.6750], zoom=14,
           style={'width': '1000px', 'height': '500px'}),
    html.Div(id="log")
])

# Callback for interactivity.
@app.callback(Output("log", "children"), Input(dict(id=ALL), "n_clicks"))
def log_position(_):
    idx = json.loads(dash.callback_context.triggered[0]['prop_id'].split(".")[0])["id"]
    location = locations[idx]
    print(location)  # print location to console
    return location  # print location to ui

if __name__ == '__main__':
    app.run_server()

I wasn't sure what you meant by "copy", so I have demonstrated in the example how the location can be accessed both in the Python layer (printed to console) and in the ui layer (written to a div).

The demo app in action

emher
  • 5,634
  • 1
  • 21
  • 32