Streamlit-Folium Issue: Implementing a refresh button and retaining center and zoom locations

Hello all,

I am currently working on an app which is running locally - I am hoping to deploy it soon!

I’m using:

  • Python 3.11
  • Streamlit 1.29.0,
  • Streamlit-Folium 0.17.4
  • IDE is PyCharm 2023.3.1

Here is what I’m currently working through:

I’m using the UK Department for Transport’s (DfT) open bus data API to fetch, process, and display live bus locations across London.

Fetching, processing, and displaying the bus data is simple and easy to do.

I’m currently using st_folium() to display the bus locations as marker clusters - mainly because there are around 8,300 buses out and about (which can be resource intensive to plot individual). This is fine though as when I zoom in the individually markers appear.

I usually use folium_static() and have made the switch to st_folium() to start making use of more interactivity in my maps.

This is my problem:

I want to implement a refresh button that reloads the bus data and takes the user back to the last known center and zoom location so this creates the effect of seeing the buses move in real-time (not perfect but it should do the trick for what I need it for).

The bus location data is updates every 10-30 seconds on the host’s side (DfT).

I know st_folium() returns a dictionary with variables such as zoom, center, last clicked, etc, etc.

I also know that accessing the session state is important to utilising these values.

However, I’ve tried all sorts to successfully add the refresh functionality and can’t seem to get it working.

I run into issues such as:

  • The map just reloads the default CENTER_START and ZOOM_START locations.
  • The streamlit app just goes into an infinite refresh loop.

Does anyone have experience of implementing a similar function into their maps and did you face similar issues?

Any help would be great!

Many thanks,
Chris

Here is my current code which currently works fine without the refresh functionality:

import folium
from folium.plugins import MarkerCluster
import streamlit as st
from bod_api import fetch_and_save_bus_data
from xml_to_parquet import xml_to_parquet
from geo_prep import load_data, create_gdf
from streamlit_folium import st_folium

# Set page config and title
st.set_page_config(layout="wide")
st.markdown("# :bus: Live London Bus Map")

# Set up initial map location
CENTER_START = [51.5074, -0.1278]
ZOOM_START = 12


def load_and_prepare_data():
    # Call DfT API for bus data and convert xml into a parquet file
    # Create a geo dataframe from the parquet (CRS is 4328)
    api_key = st.secrets["tfl_bus"]["api_key"]
    api_url = st.secrets["tfl_bus"]["api_url"]
    bounding_box = st.secrets["tfl_bus"]["bounding_box"]

    fetch_and_save_bus_data(api_url, bounding_box, api_key, "bus_location_data.xml")
    xml_to_parquet("bus_location_data.xml", "bus_location_data_combined.parquet")

    data = load_data("bus_location_data_combined.parquet")
    geo_df = create_gdf(data)
    return geo_df


def initialize_session_state():
    # Set session state variables
    if "center" not in st.session_state:
        st.session_state["center"] = CENTER_START
    if "zoom" not in st.session_state:
        st.session_state["zoom"] = ZOOM_START
    if "map_data" not in st.session_state:
        st.session_state["map_data"] = {}


def add_markers_to_cluster(geo_df, marker_cluster):
    # Iterate over rows to create markers
    for index, row in geo_df.iterrows():
        # Set the colour based on OperatorRef
        icon_color = 'red' if row['OperatorRef'] == 'TFLO' else 'blue'

        # Create popup content
        popup_content = f"""
        <b>Bus Route:</b> {row['PublishedLineName']}<br>
        <b>Direction:</b> {row['DirectionRef']}<br>
        <b>Operator:</b> {row['OperatorRef']}
        """

        # Add marker to marker cluster
        folium.Marker(
            location=[row.geometry.y, row.geometry.x],
            icon=folium.Icon(color=icon_color, icon="bus", prefix="fa"),
            popup=popup_content,
            tooltip=row['PublishedLineName']
        ).add_to(marker_cluster)


def create_map(center, zoom):
    # Create the map with the marker cluster
    m = folium.Map(location=center, zoom_start=zoom)
    marker_cluster = MarkerCluster().add_to(m)
    return m, marker_cluster


# Load session state
initialize_session_state()

# Load and prepare data
geo_df = load_and_prepare_data()

# Display the map
m, marker_cluster = create_map(st.session_state["center"], st.session_state["zoom"])
add_markers_to_cluster(geo_df, marker_cluster)
map_data = st_folium(m, center=st.session_state["center"], zoom=st.session_state["zoom"],
                     width=1285, height=800, key='new', returned_objects=[])

I’ve tried implementing a def main() function like this and I think this ended up putting the app into an infinite refresh for some reason.

def main():
    initialize_session_state()

    # Add a refresh button
    if st.button("Refresh"):
        geo_df = load_and_prepare_data()
    else:
        if "geo_df" not in st.session_state:
            st.session_state["geo_df"] = load_and_prepare_data()
        geo_df = st.session_state["geo_df"]

    m, marker_cluster = create_map(st.session_state["center"], st.session_state["zoom"])
    add_markers_to_cluster(geo_df, marker_cluster)

    # Handle map interactions
    map_data = st_folium(m, center=st.session_state["center"], zoom=st.session_state["zoom"], width=1285, height=800, key='map')
    
    if map_data:
        if map_data.get("last_center"):
            st.session_state["center"] = map_data["last_center"]
        if map_data.get("last_zoom"):
            st.session_state["zoom"] = map_data["last_zoom"]

if __name__ == "__main__":
    main()

What happens if you don’t pass a key to st_folium?