randyzwitch / streamlit-folium

Streamlit Component for rendering Folium maps
https://folium.streamlit.app/
MIT License
468 stars 176 forks source link

folium.LayerControl() does not work with dynamic feature group #153

Closed patrontheo closed 9 months ago

patrontheo commented 10 months ago

Hello,

folium.LayerControl() does not work when adding a feature group dynamically with st_folium(). Also, it would be super useful to be able to add a list of feature groups to be added dynamically (instead of a single one), as in the following:

map = st_folium(
    map,
    feature_group_to_add = [fg1, fg2]
)

Each one of the provided feature group should be 'tickable' in the layer control.

Here is a sample code for you to see that folium.LayerControl() does not work.

import folium
import geopandas as gpd
import shapely
import streamlit as st

from streamlit_folium import st_folium

START_LOCATION = [37.7934347109497, -122.399077892527]
START_ZOOM = 18

wkt = (
    "POLYGON ((-122.399077892527 37.7934347109497, -122.398922660838 "
    "37.7934544916178, -122.398980265018 37.7937266504805, -122.399133972495 "
    "37.7937070646238, -122.399077892527 37.7934347109497))"
)
polygon_ = shapely.wkt.loads(wkt)
gdf = gpd.GeoDataFrame(geometry=[polygon_]).set_crs(epsg=4326)

polygon_folium = folium.GeoJson(data=gdf)

map = folium.Map(
    location=START_LOCATION, zoom_start=START_ZOOM, tiles="OpenStreetMap", max_zoom=21
)
fg = folium.FeatureGroup(name="Parcels")
fg = fg.add_child(polygon_folium)

folium.LayerControl().add_to(map)

st_folium(
    map,
    width=800,
    height=450,
    feature_group_to_add=fg,
    debug=True,
)

As you can see, the layer control does not appear. Thank you

patrontheo commented 10 months ago

@randyzwitch Any pointers to where I could have a look, and maybe try to do a PR ? (not sure I have the required skills though)

randyzwitch commented 10 months ago

Unfortunately, with both this one and the other issue you opened, it's not immediately obvious to me without digging into the code what the issue is (I'm surprisingly uninformed about folium 🥲)

Best thing to do is just try and see where it gets you

patrontheo commented 10 months ago

Ok, thanks for the answer! I'll try to give it a shot when I have a bit of time

blackary commented 10 months ago

@patrontheo One clarification that might be helpful -- the purpose of feature_group_to_add is explicitly only for cases where you want to dynamically update a feature group while interacting with your app. If you don't need that, you can just add it directly to the map, and don't pass anything to feature_group_to_add.

Maybe that's obvious, and you did say "dynamic feature group", but just wanted to make sure that's clear.

patrontheo commented 10 months ago

@blackary yes, basically in my app I have a table on the left and a map on the right, and based on which row of the table is selected, I display different polygons on the map. So to avoid a reload of the map at each row selection, I would like to use the feature_group_to_add. As I display different things (parcles boundaries, building boundaries, and other layers), it is useful to select which layers to display or not, so to have a layer control also on dynamically added feature groups.

The possibility of giving a list of feature groups instead of a single one is more of a feature request that I think could be nice to have :), but of course I know that you've other things to do, so nothing urgent there ;)

randyzwitch commented 10 months ago

The possibility of giving a list of feature groups instead of a single one

I would guess that's easier to add than you might expect, probably would just be on the Python code side? If I'm thinking about it correctly, you would do something like test the feature group variable usingisinstance(list), then write the logic to add the feature groups to the existing object in the proper field.

patrontheo commented 10 months ago

Yeah, I tried that and it works, partially.. Actually it led me to a new issue: if you remove a dynamic feature group (setting feature_group_to_add to None), it is not removed from the map. I think it comes from here, where the removeLayer mewthod is called only if the feature group is not None.

Here is some sample code to show that (basically clicking the button "Remove Feature Group" will not work):

import folium
import geopandas as gpd
import shapely
import streamlit as st

from streamlit_folium import st_folium

st.set_page_config(layout='wide', initial_sidebar_state='collapsed')
st.write('<style>div.block-container{padding-top:2rem;}</style>', unsafe_allow_html=True) # remove padding top

START_LOCATION = [37.7944347109497, -122.398077892527]
START_ZOOM = 17

if not 'feature_group' in st.session_state:
    st.session_state['feature_group'] = None

wkt1 = "POLYGON ((-122.399077892527 37.7934347109497, -122.398922660838 37.7934544916178, -122.398980265018 37.7937266504805, -122.399133972495 37.7937070646238, -122.399077892527 37.7934347109497))"

polygon_1 = shapely.wkt.loads(wkt1)

gdf1 = gpd.GeoDataFrame(geometry=[polygon_1]).set_crs(epsg=4326)

style_parcels1 = {'fillColor': '#1100f8', 'color': '#1100f8', 'fillOpacity': 0.13, 'weight': 2 }

polygon_folium1 = folium.GeoJson(data=gdf1, style_function=lambda x: style_parcels1)

map = folium.Map(
    location=START_LOCATION, zoom_start=START_ZOOM, tiles="OpenStreetMap", max_zoom=21
)

fg1 = folium.FeatureGroup(name="Parcels")
fg1.add_child(polygon_folium1)

def add_fg_to_ss(fg):
    st.session_state["feature_group"] = fg

def remove_fg_from_ss():
    st.session_state["feature_group"] = None

b1 = st.button("Add Feature Group", on_click=add_fg_to_ss, args=(fg1,))
b2 = st.button("Remove Feature Group", on_click=remove_fg_from_ss)

st_folium(
    map,
    width=800,
    height=450,
    returned_objects=[],
    feature_group_to_add=st.session_state['feature_group'],
    debug=False,
)

Should I open a new issue for this one ?

For giving a list of feature groups, it kind of work, but with the current implementation in index.tsx, the code only removes one layer, and would need to be modified to remove a list of layers when the feature group list is updated. Perhaps something like the following could work:

    if (Array.isArray(window.feature_group)) {
      window.feature_group.forEach((group) => {
        window.map.removeLayer(group)
      })
    }

Maybe I'll have more time later to try to implement it, but I'm not sure.

randyzwitch commented 10 months ago

Should I open a new issue for this one ?

In general, the answer is almost always yes, open a new issue. Or if they all are related to the same functionality, just write up one meta-issue describing the desired implementation/result behavior

patrontheo commented 10 months ago

@randyzwitch I submitted a PR. However for the original topic of this issue, which is making Layercontrol available for dynamically added feature groups, I don't think I have the technical knowledge to do it. Perhaps with some pointers I can dig a bit but I'm not sure where to look at for now.

patrontheo commented 10 months ago

Actually I think I got it, I'll try to add some tests and submit a PR later