From f7648a8c90a5a776e6fd2eb98884cda798beaf1d Mon Sep 17 00:00:00 2001 From: Zachary Blackwood Date: Tue, 3 Jan 2023 12:55:12 -0500 Subject: [PATCH 1/2] Fix bug, isolate string generation, add more tests --- examples/pages/draw_support.py | 3 +- examples/pages/limit_data_return.py | 2 +- examples/pages/misc_examples.py | 6 +- streamlit_folium/__init__.py | 146 ++++++++++++++++++---------- tests/test_package.py | 110 +++++++++++---------- 5 files changed, 154 insertions(+), 113 deletions(-) diff --git a/examples/pages/draw_support.py b/examples/pages/draw_support.py index 39e2a9d..0c6328a 100644 --- a/examples/pages/draw_support.py +++ b/examples/pages/draw_support.py @@ -18,8 +18,7 @@ Draw something below to see the return value back to Streamlit! """ -with st.echo(): - +with st.echo(code_location="below"): import folium import streamlit as st from folium.plugins import Draw diff --git a/examples/pages/limit_data_return.py b/examples/pages/limit_data_return.py index ab7447d..e53da69 100644 --- a/examples/pages/limit_data_return.py +++ b/examples/pages/limit_data_return.py @@ -22,7 +22,7 @@ """ -with st.echo(): +with st.echo(code_location="below"): import folium import streamlit as st diff --git a/examples/pages/misc_examples.py b/examples/pages/misc_examples.py index 4e5627d..7c26cb4 100644 --- a/examples/pages/misc_examples.py +++ b/examples/pages/misc_examples.py @@ -5,6 +5,8 @@ from streamlit_folium import st_folium +st.set_page_config(layout="wide") + page = st.radio("Select map type", ["Single map", "Dual map", "Branca figure"], index=0) # center on Liberty Bell, add marker @@ -12,10 +14,6 @@ with st.echo(): m = folium.Map(location=[39.949610, -75.150282], zoom_start=16) tooltip = "Liberty Bell" - legend_img = "https://placekitten.com/200/300" - url = "https://raw.githubusercontent.com/SECOORA/static_assets/master/maps/img/rose.png" - szt = folium.plugins.FloatImage(url, bottom=60, left=70, width="20%") - m.add_child(szt) folium.Marker( [39.949610, -75.150282], popup="Liberty Bell", tooltip=tooltip ).add_to(m) diff --git a/streamlit_folium/__init__.py b/streamlit_folium/__init__.py index b6b9c1f..d6d72ea 100644 --- a/streamlit_folium/__init__.py +++ b/streamlit_folium/__init__.py @@ -27,7 +27,7 @@ _component_func = components.declare_component("st_folium", path=build_dir) -def generate_js_hash(js_string: str, key: str = None) -> str: +def generate_js_hash(js_string: str, key: str | None = None) -> str: """ Generate a standard key from a javascript string representing a series of folium-generated leaflet objects by replacing the hash's at the end @@ -99,6 +99,75 @@ def folium_static( return st_folium(fig, width=width, height=height, returned_objects=[]) +def _get_siblings(fig: folium.MacroElement) -> str: + """Get the html for any siblings of the map""" + fig.render() + children = list(fig.get_root()._children.values()) + + html = "" + if len(children) > 1: + for child in children[1:]: + try: + html += child._template.module.html() + "\n" + except Exception: + pass + + return html + + +def get_full_id(m: folium.MacroElement) -> str: + if isinstance(m, folium.plugins.DualMap): + m = m.m1 + + return f"{m._name.lower()}_{m._id}" + + +def _get_map_string(fig: folium.Map) -> str: + fig.render() + + leaflet = generate_leaflet_string(fig) + + # Get rid of the annoying popup + leaflet = leaflet.replace("alert(coords);", "") + + leaflet = dedent(leaflet) + + if "drawnItems" not in leaflet: + leaflet += "\nvar drawnItems = [];" + + # Replace the folium generated map_{random characters} variables + # with map_div and map_div2 (these end up being both the assumed) + # div id where the maps are inserted into the DOM, and the names of + # the variables themselves. + if isinstance(fig, folium.plugins.DualMap): + m2_id = get_full_id(fig.m2) + leaflet = leaflet.replace(m2_id, "map_div2") + + return leaflet + + +def _get_feature_group_string( + feature_group_to_add: folium.FeatureGroup, + map: folium.Map, +) -> str: + feature_group_to_add._id = "feature_group" + feature_group_to_add.add_to(map) + feature_group_string = generate_leaflet_string( + feature_group_to_add, base_id="feature_group" + ) + m_id = get_full_id(map) + feature_group_string = feature_group_string.replace(m_id, "map_div") + feature_group_string = dedent(feature_group_string) + + feature_group_string += dedent( + """ + map_div.addLayer(feature_group_feature_group); + window.feature_group = feature_group_feature_group; + """ + ) + return feature_group_string + + def st_folium( fig: folium.MacroElement, key: str | None = None, @@ -150,41 +219,18 @@ def st_folium( # "default" is a special argument that specifies the initial return # value of the component before the user has interacted with it. + folium_map: folium.Map = fig # type: ignore + # handle the case where you pass in a figure rather than a map # this assumes that a map is the first child - fig.render() - if not (isinstance(fig, folium.Map) or isinstance(fig, folium.plugins.DualMap)): - fig = list(fig._children.values())[0] + folium_map = list(fig._children.values())[0] - leaflet = generate_leaflet_string(fig, base_id="map_div") + leaflet = _get_map_string(folium_map) # type: ignore - children = list(fig.get_root()._children.values()) + html = _get_siblings(folium_map) - html = "" - if len(children) > 1: - for child in children[1:]: - try: - html += child._template.module.html() + "\n" - except Exception: - pass - - # Replace the folium generated map_{random characters} variables - # with map_div and map_div2 (these end up being both the assumed) - # div id where the maps are inserted into the DOM, and the names of - # the variables themselves. - if isinstance(fig, folium.plugins.DualMap): - m_id = get_full_id(fig.m1) - m2_id = get_full_id(fig.m2) - leaflet = leaflet.replace(m2_id, "map_div2") - else: - m_id = get_full_id(fig) - - # Get rid of the annoying popup - leaflet = leaflet.replace("alert(coords);", "") - - if "drawnItems" not in leaflet: - leaflet += "\nvar drawnItems = [];" + m_id = get_full_id(folium_map) def bounds_to_dict(bounds_list: List[List[float]]) -> Dict[str, Dict[str, float]]: southwest, northeast = bounds_list @@ -200,7 +246,7 @@ def bounds_to_dict(bounds_list: List[List[float]]) -> Dict[str, Dict[str, float] } try: - bounds = fig.get_bounds() + bounds = folium_map.get_bounds() except AttributeError: bounds = [[None, None], [None, None]] @@ -210,7 +256,9 @@ def bounds_to_dict(bounds_list: List[List[float]]) -> Dict[str, Dict[str, float] "all_drawings": None, "last_active_drawing": None, "bounds": bounds_to_dict(bounds), - "zoom": fig.options.get("zoom") if hasattr(fig, "options") else {}, + "zoom": folium_map.options.get("zoom") + if hasattr(folium_map, "options") + else {}, "last_circle_radius": None, "last_circle_polygon": None, } @@ -227,17 +275,10 @@ def bounds_to_dict(bounds_list: List[List[float]]) -> Dict[str, Dict[str, float] # on the frontend. feature_group_string = None if feature_group_to_add is not None: - feature_group_to_add._id = "feature_group" - feature_group_to_add.add_to(fig) - feature_group_string = generate_leaflet_string( - feature_group_to_add, base_id="feature_group" + feature_group_string = _get_feature_group_string( + feature_group_to_add, + map=folium_map, ) - m_id = get_full_id(fig) - feature_group_string = feature_group_string.replace(m_id, "map_div") - feature_group_string += """ - map_div.addLayer(feature_group_feature_group); - window.feature_group = feature_group_feature_group; - """ component_value = _component_func( script=leaflet, @@ -256,12 +297,6 @@ def bounds_to_dict(bounds_list: List[List[float]]) -> Dict[str, Dict[str, float] return component_value -def get_full_id(m: folium.MacroElement) -> str: - if isinstance(m, folium.plugins.DualMap): - m = m.m1 - return f"{m._name.lower()}_{m._id}" - - def _generate_leaflet_string( m: folium.MacroElement, nested: bool = True, @@ -277,12 +312,19 @@ def _generate_leaflet_string( if isinstance(m, folium.plugins.DualMap): if not nested: - return _generate_leaflet_string(m.m1, nested=False, mappings=mappings) + return _generate_leaflet_string( + m.m1, nested=False, mappings=mappings, base_id=base_id + ) # Generate the script for map1 - leaflet, _ = _generate_leaflet_string(m.m1, nested=nested, mappings=mappings) + leaflet, _ = _generate_leaflet_string( + m.m1, nested=nested, mappings=mappings, base_id=base_id + ) # Add the script for map2 leaflet += ( - "\n" + _generate_leaflet_string(m.m2, nested=nested, mappings=mappings)[0] + "\n" + + _generate_leaflet_string( + m.m2, nested=nested, mappings=mappings, base_id="div2" + )[0] ) # Add the script that syncs them together leaflet += m._template.module.script(m) @@ -313,7 +355,7 @@ def _generate_leaflet_string( def generate_leaflet_string( - m: folium.MacroElement, nested: bool = True, base_id: str = "0" + m: folium.MacroElement, nested: bool = True, base_id: str = "div" ) -> str: """ Call the _generate_leaflet_string function, and then replace the diff --git a/tests/test_package.py b/tests/test_package.py index dc2b177..c3504cd 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,50 +1,13 @@ -from textwrap import dedent - -old = """ -from seleniumbase import BaseCase -import cv2 -import time - - -class ComponentsTest(BaseCase): - def test_basic(self): - - # open the app and take a screenshot - self.open("http://localhost:8501") - time.sleep(10) - self.save_screenshot("current-screenshot.png") - - # automated visual regression testing - # tests page has identical structure to baseline - # https://github.com/seleniumbase/SeleniumBase/tree/master/examples/visual_testing - # level 2 chosen, as id values dynamically generated on each page run - self.check_window(name="first_test", level=2) - - # check folium app-specific parts - # automated test level=2 only checks structure, not content - self.assert_text("streamlit-folium") - - # test screenshots look exactly the same - original = cv2.imread("visual_baseline/test_basic/first_test/baseline.png") - duplicate = cv2.imread("current-screenshot.png") - - assert original.shape == duplicate.shape - - difference = cv2.subtract(original, duplicate) - b, g, r = cv2.split(difference) - assert cv2.countNonZero(b) == cv2.countNonZero(g) == cv2.countNonZero(r) == 0 -""" - - def test_map(): import folium - from streamlit_folium import generate_leaflet_string + from streamlit_folium import _get_map_string map = folium.Map() - leaflet = generate_leaflet_string(map) - assert """var map_0 = L.map( - "map_0", + leaflet = _get_map_string(map) + assert ( + """var map_div = L.map( + "map_div", { center: [0, 0], crs: L.CRS.EPSG3857, @@ -52,13 +15,13 @@ def test_map(): zoomControl: true, preferCanvas: false, } -);""" in dedent( - leaflet +);""" + in leaflet ) - assert "var tile_layer_0_0 = L.tileLayer(" in leaflet + assert "var tile_layer_div_0 = L.tileLayer(" in leaflet - assert ").addTo(map_0);" in leaflet + assert ").addTo(map_div);" in leaflet def test_layer_control(): @@ -70,33 +33,72 @@ def test_layer_control(): folium.LayerControl().add_to(map) map.render() leaflet = generate_leaflet_string(map) - assert "var tile_layer_0_0 = L.tileLayer(" in leaflet - assert '"openstreetmap" : tile_layer_0_0,' in leaflet + assert "var tile_layer_div_0 = L.tileLayer(" in leaflet + assert '"openstreetmap" : tile_layer_div_0,' in leaflet def test_draw_support(): import folium from folium.plugins import Draw - from streamlit_folium import generate_leaflet_string + from streamlit_folium import _get_map_string map = folium.Map() Draw(export=True).add_to(map) map.render() - leaflet = dedent(generate_leaflet_string(map)) - assert "map_0.on(L.Draw.Event.CREATED, function(e) {" in leaflet + leaflet = _get_map_string(map) + assert "map_div.on(L.Draw.Event.CREATED, function(e) {" in leaflet assert "drawnItems.addLayer(layer);" in leaflet assert ( - """map_0.on('draw:created', function(e) { + """map_div.on('draw:created', function(e) { drawnItems.addLayer(e.layer); });""" in leaflet ) assert ( - """var draw_control_0_1 = new L.Control.Draw( + """var draw_control_div_1 = new L.Control.Draw( options -).addTo( map_0 );""" +).addTo( map_div );""" in leaflet ) + + assert "alert" not in leaflet + + +def test_map_id(): + import folium + + from streamlit_folium import _get_map_string + + map = folium.Map() + leaflet = _get_map_string(map) + assert "var map_div = L.map(" in leaflet + + +def test_feature_group(): + import folium + + from streamlit_folium import _get_feature_group_string + + fg = folium.FeatureGroup() + m = folium.Map() + + fg_str = _get_feature_group_string(fg, m) + + assert "var feature_group_feature_group = L.featureGroup(" in fg_str + assert ".addTo(map_div);" in fg_str + + +def test_dual_map(): + import folium.plugins + + from streamlit_folium import _get_map_string + + dual_map = folium.plugins.DualMap() + dual_map.render() + map_str = _get_map_string(dual_map) + + assert "var map_div = L.map(" in map_str + assert "var map_div2 = L.map(" in map_str From e5210cad967fb80cedfbf3df9b7267e85c30aa2b Mon Sep 17 00:00:00 2001 From: Zachary Blackwood Date: Tue, 3 Jan 2023 13:22:37 -0500 Subject: [PATCH 2/2] Add VectorGrid support --- requirements.txt | 2 +- setup.py | 2 +- streamlit_folium/frontend/public/index.html | 2 ++ tests/test_package.py | 13 +++++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a6658c2..d938bdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ streamlit>=1.2.0 -folium>=0.11 +folium>=0.13 jinja2 branca diff --git a/setup.py b/setup.py index 6781caa..8efa2d5 100644 --- a/setup.py +++ b/setup.py @@ -13,5 +13,5 @@ include_package_data=True, classifiers=[], python_requires=">=3.7", - install_requires=["streamlit>=1.2", "folium>=0.11", "jinja2", "branca"], + install_requires=["streamlit>=1.2", "folium>=0.13", "jinja2", "branca"], ) diff --git a/streamlit_folium/frontend/public/index.html b/streamlit_folium/frontend/public/index.html index 7779362..a3dcf42 100644 --- a/streamlit_folium/frontend/public/index.html +++ b/streamlit_folium/frontend/public/index.html @@ -49,6 +49,8 @@ + +