widgetti / solara

A Pure Python, React-style Framework for Scaling Your Jupyter and Web Apps
https://solara.dev
MIT License
1.9k stars 141 forks source link

Inconsistent issue when removing a split map control from an ipyleaflet map #850

Open lopezvoliver opened 1 week ago

lopezvoliver commented 1 week ago

I am facing a very difficult issue to trace, so I came up with this almost reproducible example.

This animation shows the demo initially working as expected. Then, I refresh the app and there is an issue with the demo: the split-map control does not get removed correctly.

bug_split_map_inconsistent

Setup

Let's start with defining a custom class inheriting from ipyleaflet.Map that can dynamically change between a split map and a "stack" map (layers stacked on top of each other).

import ipyleaflet
import traitlets
from traitlets import observe

class Map(ipyleaflet.Map,):
    map_type = traitlets.Unicode().tag(sync=True)

    @observe("map_type")
    def _on_map_type_change(self, change):
        if hasattr(self, "split_map_control"):
            if change.new=="stack":
                self.remove(self.split_map_control)  
                self.set_stack_mode()

            if change.new=="split":
                self.set_split_mode()     

    def set_stack_mode(self):
       self.layers = tuple([
           self.esri_layer,
           self.topo_layer
       ]) 

    def set_split_mode(self):
        self.layers = ()
        self.add(self.left_layer)
        self.add(self.right_layer)
        self.add(self.split_map_control)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.osm = self.layers[0]

        esri_url=ipyleaflet.basemaps.Esri.WorldImagery.build_url()
        topo_url = ipyleaflet.basemaps.OpenTopoMap.build_url()

        self.left_layer = ipyleaflet.TileLayer(url = topo_url, name="left")
        self.right_layer = ipyleaflet.TileLayer(url = esri_url, name="right")
        self.topo_layer = ipyleaflet.TileLayer(url=topo_url, name="topo", opacity=0.25)
        self.esri_layer = ipyleaflet.TileLayer(url=esri_url, name="esri")

        self.stack_layers = [
            self.esri_layer,
            self.topo_layer,
        ]

        self.split_map_control = ipyleaflet.SplitMapControl(
            left_layer=self.left_layer, 
            right_layer=self.right_layer)

        if self.map_type=="split":
            self.set_split_mode()

        if self.map_type=="stack":
            self.set_stack_mode()

I haven't encountered the issue when testing the ipyleaflet code without solara.

Now let's add solara to the equation:

import solara
import ipyleaflet
import traitlets
from traitlets import observe
zoom=solara.reactive(4)
map_type = solara.reactive("stack")

class Map(ipyleaflet.Map,):
.... (same code defining Map as above)
....

@solara.component
def Page():
    with solara.ToggleButtonsSingle(value=map_type):
       solara.Button("Stack", icon_name="mdi-layers-triple", value="stack", text=True)
       solara.Button("Split", icon_name="mdi-arrow-split-vertical", value="split", text=True)   
    Map.element(
        zoom=zoom.value,
        on_zoom=zoom.set,
        map_type=map_type.value
    ) 

Page()

Could you please help diagnose this problem?

Here's a live version of an app where I am facing this issue (also inconsistently! try refreshing until you encounter the issue).

iisakkirotko commented 1 day ago

Hey @lopezvoliver! I looked into this, and it turned out to be quite complicated. The cause of the issue is two-fold:

There are some workarounds you could try: