Search code examples
javascriptpythonfolium

How to copy a marker's location on folium Map by clicking on it?



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,


Solution

  • 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)