Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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,

like image 406
Mistapopo Avatar asked May 20 '21 21:05

Mistapopo


People also ask

How can you add marking objects on a Folium map?

Folium gives a folium. Marker() class for plotting markers on a map. Just pass the latitude and longitude of the location, mention the popup and tooltip and add it to the map.

Can Folium use Google Maps?

We used the geopy library to get the latitude and longitude of the location. Then we used the "folium. map" method of the folium package for creating the base of Google Maps. In step 2, we used "folium.

How do I change the map type in Folium?

The basic code for changing the map tiles used by folium is to first add a specified tile layer to the map (in this case “my_map”). You can also add a control for switching between layers with the “LayerControl()” method. # other mapping code (e.g. lines, markers etc.)


2 Answers

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)
like image 172
RJ Adriaansen Avatar answered Sep 29 '22 12:09

RJ Adriaansen


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

like image 35
emher Avatar answered Sep 29 '22 12:09

emher