Hello, I am new to Panel and I am building an interactive GUI whose inputs are a string from a Select widget, and a pair of coordinates obtained from a Folium Map. I am having trouble fetching coordinates back to Python.
In a first version of my code, I used the ClickforLatLng shortcut and used the clipboard to set the coordinates. However, I want to implement my code on a server so this workaround is not suitable anymore. Right now I am focusing on creating a Custom widget with ReactiveHTML, but so far it is not working.
For more context, my problem is very similar as documented here, only I just want the two coordinates to be stored in one or two python variables after each click, and then retrieved when another button is pressed and run into the code.
After reviewing the documentation and other forum posts I have not reached a working solution, so I want to know if perhaps other users arrived at a good implementation. Thank you in advance!
EDIT: The following code works! It creates a folium map instance and prints the last-clicked coordinates when the button is clicked.
import folium
from folium.elements import MacroElement
from jinja2 import Template
import panel as pn
import param
class LatLngPopup(MacroElement):
"""
When one clicks on a Map that contains a LatLngPopup,
a popup is shown that displays the latitude and longitude of the pointer.
"""
_template = Template(u"""
{% macro script(this, kwargs) %}
var {{this.get_name()}} = L.popup();
function latLngPop(e) {
var lat = e.latlng.lat.toFixed(4);
var lng = e.latlng.lng.toFixed(4);
// Correctly set the data attributes using querySelector
var el = parent.document.querySelector("[id^='location-widget']");
if (el) {
el.setAttribute("data-latitude", lat);
el.setAttribute("data-longitude", lng);
}
{{this.get_name()}}
.setLatLng(e.latlng)
.setContent('Latitude: ' + lat + '<br>Longitude: ' + lng)
.openOn({{this._parent.get_name()}});
}
{{this._parent.get_name()}}.on('click', latLngPop);
{% endmacro %}
""")
def __init__(self):
super(LatLngPopup, self).__init__()
self._name = 'LatLngPopup'
# Define the custom Location class with Param
class Location(param.Parameterized):
latitude = param.Number(default=37.7749)
longitude = param.Number(default=-122.4194)
# Define the custom ReactiveHTML component
class LocationWidget(pn.reactive.ReactiveHTML):
latitude = param.Number()
longitude = param.Number()
_template = """
<div id="location-widget-${id}"
data-latitude="${latitude}"
data-longitude="${longitude}">
</div>
"""
_scripts = {
'render': """
var locWidget = document.querySelector("[id^='location-widget']");
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes') {
var lat = locWidget.getAttribute('data-latitude');
var lng = locWidget.getAttribute('data-longitude');
data.latitude = parseFloat(lat);
data.longitude = parseFloat(lng);
}
});
});
observer.observe(locWidget, { attributes: true });
"""
}
# Initialize the Panel extension
pn.extension()
# Create an instance of the custom class
location = Location()
# Create an instance of the custom component with initial values
location_widget = LocationWidget(id='unique', latitude=location.latitude, longitude=location.longitude)
# Button to trigger location info retrieval
button = pn.widgets.Button(name='Get Location Info')
button.on_click(lambda event: print(f"Latitude: {location_widget.latitude}, Longitude: {location_widget.longitude}"))
# Create a folium map
m = folium.Map(location=[45.5236, -122.6750], zoom_start=13)
# Add LatLngPopup to the map
popup = LatLngPopup()
m.add_child(popup)
# Create a panel pane to display the map
map_pane = pn.pane.plot.Folium(m, height=500, sizing_mode='stretch_width')
# Arrange the layout
layout = pn.Row(map_pane, button, pn.panel(location_widget))
# Serve the Panel app
layout.servable()