Access to _script of ReactiveHTML

Hi,
I am using ReactiveHTML to develop a spcialized class using Leaflet. I could not use ipyleaflet, as I needed to introduce additional functions on the events of the map.

Now, what I can’t wrap my head around is this;
Beacuse the leaflet map is actually hold in a variable in the _script in the class. Is it possible to access to the map instance (state.map) from the panel objects or somehow link with a js callback from panel widgets, like the run_button. What I want to do is add some layers etc to the map when the run_button is clicked.

My reasoning of having the run_button as a panel widget, instead of also declaring it in the LeafletMap class is it needs to run some background jobs on the server.

I am posting my simplest code (which was based on a sample of Marc):

from panel.reactive import ReactiveHTML
import param
import panel as pn

CSS = ['https://unpkg.com/leaflet@1.8.0/dist/leaflet.css',
]


JS = {
    'leaflet': 'https://unpkg.com/leaflet@1.8.0/dist/leaflet.js',
}

pn.extension(css_files=CSS, js_files=JS)

class LeafletMap(ReactiveHTML):
    
    attribution = param.String(doc="Tile source attribution.")
    center = param.XYCoordinates(default=(43, 34), doc="The center of the map.")
    # data = param.DataFrame(doc="The heatmap data to plot, should have 'x', 'y' and 'value' columns.")
    tile_url = param.String(doc="Tile source URL with {x}, {y} and {z} parameter")
    zoom = param.Integer(5, bounds=(0, 21), doc="The map zoom-level")
    lat_input = param.Number(44.0, step=0.1, softbounds=(-90, 90), doc="Latitude of start point")
    lon_input = param.Number(34.0, step=0.1, softbounds=(-180, 180), doc="Longitude of start point")


    _template = """
    <div id="map" style="width: 100%; height: 100%; position: absolute;"></div>
    """
    
    _scripts = {
        'render': """
            state.map = L.map(map).setView(data.center, data.zoom);
            state.map.on('zoom', (e) => { data.zoom = state.map.getZoom() })
            state.marker = L.marker({lat:data.lat_input, lng:data.lon_input}, {draggable:true, title:'Baslangic Noktasi'}).addTo(state.map)
            state.marker.on('moveend', (e) => {
                data.lat_input = e.target.getLatLng().lat
                data.lon_input = e.target.getLatLng().lng
                 })            
            state.tileLayer = L.tileLayer(data.tile_url, {
                attribution: data.attribution,
                maxZoom: 21,
                tileSize: 512,
                zoomOffset: -1,
            }).addTo(state.map);
        """,
        'after_layout': """
           state.map.invalidateSize()
        """,
        'lon_input': "state.marker.setLatLng({lat:state.marker.getLatLng().lat, lng:data.lon_input})",
        'lat_input': "state.marker.setLatLng({lat:data.lat_input, lng:state.marker.getLatLng().lng})",
        'zoom': "state.map.setZoom(data.zoom)",
    }
    
    __css__ = CSS
    
    __javascript__ = list(JS.values())


map = LeafletMap(
    attribution='Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012',
    min_height=500,
    tile_url='https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
    sizing_mode='stretch_both',
    value='mag',
    zoom=5,
    height=700, 
    width = 1200
)

map_controls = pn.Column(pn.pane.Markdown("## Settings"), 
    map.controls(parameters=['lat_input', 'lon_input'],
     widgets={
        'lat_input': {'widget_type':pn.widgets.FloatInput, 'format':'000.000000', 'sizing_mode':"stretch_width"},
        'lon_input': {'widget_type':pn.widgets.FloatInput, 'format':'000.000000', 'sizing_mode':"stretch_width"},
     }, show_name=False)
     )

ACCENT_COLOR = pn.template.FastListTemplate.accent_base_color

run_button = pn.widgets.Button(name='Run', button_type='primary', width=50, disabled=False)


template = pn.template.FastListTemplate(
    site="Site", title="Map Screen",
    sidebar=[pn.pane.Markdown("## "),  map_controls, run_button],
    main=[map],
    accent_base_color=ACCENT_COLOR
)

template.servable()

Hi @FBT

In theory you should be able to use param.Event. But there is a bug which is already reported. Luckily the bug report also contains a workaround. Issue #2953

The key idea is to combine the param.Event run parameter with a param.Int runs parameter. You can see below how I can use this to change the opacity of the marker.

ipyleaflet-map

from panel.reactive import ReactiveHTML
import param
import panel as pn

CSS = ['https://unpkg.com/leaflet@1.8.0/dist/leaflet.css',
]


JS = {
    'leaflet': 'https://unpkg.com/leaflet@1.8.0/dist/leaflet.js',
}

pn.extension(css_files=CSS, js_files=JS)

class LeafletMap(ReactiveHTML):
    
    attribution = param.String(doc="Tile source attribution.")
    center = param.XYCoordinates(default=(43, 34), doc="The center of the map.")
    # data = param.DataFrame(doc="The heatmap data to plot, should have 'x', 'y' and 'value' columns.")
    tile_url = param.String(doc="Tile source URL with {x}, {y} and {z} parameter")
    zoom = param.Integer(5, bounds=(0, 21), doc="The map zoom-level")
    lat_input = param.Number(44.0, step=0.1, softbounds=(-90, 90), doc="Latitude of start point")
    lon_input = param.Number(34.0, step=0.1, softbounds=(-180, 180), doc="Longitude of start point")
    run = param.Event()
    runs = param.Integer()


    _template = """
    <div id="map" style="width: 100%; height: 100%; position: absolute;"></div>
    """
    
    _scripts = {
        'render': """
            state.map = L.map(map).setView(data.center, data.zoom);
            state.map.on('zoom', (e) => { data.zoom = state.map.getZoom() })
            state.marker = L.marker({lat:data.lat_input, lng:data.lon_input}, {draggable:true, title:'Baslangic Noktasi'}).addTo(state.map)
            state.marker.on('moveend', (e) => {
                data.lat_input = e.target.getLatLng().lat
                data.lon_input = e.target.getLatLng().lng
                 })            
            state.tileLayer = L.tileLayer(data.tile_url, {
                attribution: data.attribution,
                maxZoom: 21,
                tileSize: 512,
                zoomOffset: -1,
            }).addTo(state.map);
        """,
        'after_layout': """
           state.map.invalidateSize()
        """,
        'lon_input': "state.marker.setLatLng({lat:state.marker.getLatLng().lat, lng:data.lon_input})",
        'lat_input': "state.marker.setLatLng({lat:data.lat_input, lng:state.marker.getLatLng().lng})",
        'zoom': "state.map.setZoom(data.zoom)",
        'runs': 'state.marker.setOpacity((100-data.runs*10)/100);console.log(data.runs);',
    }
    
    __css__ = CSS
    
    __javascript__ = list(JS.values())

    def __init__(self, **params):
        super().__init__(**params)

        
    @param.depends("run", watch=True)
    def _handle_click(self):
        self.runs +=1


map = LeafletMap(
    attribution='Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012',
    min_height=500,
    tile_url='https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}',
    sizing_mode='stretch_both',
    # value='mag',
    zoom=5,
    height=700, 
    width = 1200
)

map_controls = pn.Column(pn.pane.Markdown("## Settings"), 
    map.controls(parameters=['lat_input', 'lon_input'],
     widgets={
        'lat_input': {'widget_type':pn.widgets.FloatInput, 'format':'000.000000', 'sizing_mode':"stretch_width"},
        'lon_input': {'widget_type':pn.widgets.FloatInput, 'format':'000.000000', 'sizing_mode':"stretch_width"},
     }, show_name=False)
     )

ACCENT_COLOR = pn.template.FastListTemplate.accent_base_color

run_button = pn.widgets.Button.from_param(map.param.run)


template = pn.template.FastListTemplate(
    site="Site", title="Map Screen",
    sidebar=[pn.pane.Markdown("## "),  map_controls, run_button],
    main=[map],
    accent_base_color=ACCENT_COLOR
)

template.servable()

You don’t have to add the run parameter. You can also create a button seperately as you did before, but have that one increment the runs parameter.

Please showcase your app when you have developed it. And please share this tweet if you like this response. Thanks.

1 Like

HI @Marc,

This is an excellent solution. Sorry for the delay :slight_smile:

Thank you.