diff --git a/setup.py b/setup.py index 0af0ce4..7013eb2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="streamlit_folium", - version="0.6.10", + version="0.6.11", author="Randy Zwitch", author_email="randy@streamlit.io", description="Render Folium objects in Streamlit", diff --git a/streamlit_folium/__init__.py b/streamlit_folium/__init__.py index 566ca22..80470f7 100644 --- a/streamlit_folium/__init__.py +++ b/streamlit_folium/__init__.py @@ -6,6 +6,7 @@ import branca import folium import folium.plugins +import streamlit as st import streamlit.components.v1 as components from jinja2 import UndefinedError @@ -16,9 +17,13 @@ def generate_js_hash(js_string: str, key: str = None) -> str: of folium-generated leaflet objects by replacing the hash's at the end of variable names (e.g. "marker_5f9d46..." -> "marker"), and returning the hash. + + Also strip maps/, which is generated by google earth engine """ pattern = r"(_[a-z0-9]+)" standardized_js = re.sub(pattern, "", js_string) + str(key) + url_pattern = r"(maps\/[-a-z0-9]+\/)" + standardized_js = re.sub(url_pattern, "", standardized_js) + str(key) s = hashlib.sha256(standardized_js.encode()).hexdigest() return s diff --git a/streamlit_folium/frontend/src/index.tsx b/streamlit_folium/frontend/src/index.tsx index c636457..fca0cd1 100644 --- a/streamlit_folium/frontend/src/index.tsx +++ b/streamlit_folium/frontend/src/index.tsx @@ -1,157 +1,153 @@ -import { Streamlit, RenderData } from "streamlit-component-lib"; -import { debounce } from "underscore"; -import { circleToPolygon } from "./circle-to-polygon"; - -let map: any = null; +import { Streamlit, RenderData } from "streamlit-component-lib" +import { debounce } from "underscore" +import { circleToPolygon } from "./circle-to-polygon" type GlobalData = { - map: any; - lat_lng_clicked: any; - last_object_clicked: any; - last_active_drawing: any, - all_drawings: any, - bounds: any; - zoom: any; - drawn_items: any; - last_circle_radius: number; - last_circle_polygon: any; -}; - -declare var __GLOBAL_DATA__: GlobalData; + lat_lng_clicked: any + last_object_clicked: any + last_active_drawing: any + all_drawings: any + zoom: any + drawn_items: any + last_circle_radius: number | null + last_circle_polygon: any +} -/** - * The component's render function. This will be called immediately after - * the component is initially loaded, and then again every time the - * component gets new data from Python. - */ -function onRender(event: Event): void { - // Get the RenderData from the event - const data = (event as CustomEvent).detail +declare global { + interface Window { + __GLOBAL_DATA__: GlobalData + initComponent: any + map: any + } +} - const fig: string = data.args["fig"]; - const height: number = data.args["height"]; - const width: number = data.args["width"]; +function onMapClick(e: any) { + const global_data = window.__GLOBAL_DATA__ + global_data.lat_lng_clicked = e.latlng + debouncedUpdateComponentValue(window.map) +} - function onMapClick(e: any) { - const global_data = __GLOBAL_DATA__; - global_data.lat_lng_clicked = e.latlng; - debouncedUpdateComponentValue() - } +let debouncedUpdateComponentValue = debounce(updateComponentValue, 250) + +function updateComponentValue(map: any) { + const global_data = window.__GLOBAL_DATA__ + let bounds = map.getBounds() + let zoom = map.getZoom() + Streamlit.setComponentValue({ + last_clicked: global_data.lat_lng_clicked, + last_object_clicked: global_data.last_object_clicked, + all_drawings: global_data.all_drawings, + last_active_drawing: global_data.last_active_drawing, + bounds: bounds, + zoom: zoom, + last_circle_radius: global_data.last_circle_radius, + last_circle_polygon: global_data.last_circle_polygon, + }) +} - let debouncedUpdateComponentValue = debounce(updateComponentValue, 250) - - function updateComponentValue() { - const global_data = __GLOBAL_DATA__; - let map = global_data.map; - let bounds = map.getBounds(); - let zoom = map.getZoom(); - Streamlit.setComponentValue({ - last_clicked: global_data.lat_lng_clicked, - last_object_clicked: global_data.last_object_clicked, - all_drawings: global_data.all_drawings, - last_active_drawing: global_data.last_active_drawing, - bounds: bounds, - zoom: zoom, - last_circle_radius: global_data.last_circle_radius, - last_circle_polygon: global_data.last_circle_polygon, - }) - } +function onMapMove(e: any) { + debouncedUpdateComponentValue(window.map) +} - function onMapMove(e: any) { - debouncedUpdateComponentValue() - } +function onDraw(e: any) { + const global_data = window.__GLOBAL_DATA__ - function onDraw(e: any) { - const global_data = __GLOBAL_DATA__; + var type = e.layerType, + layer = e.layer - var type = e.layerType, - layer = e.layer; + if (type === "circle") { + var center: [number, number] = [layer._latlng.lng, layer._latlng.lat] + var radius = layer.options.radius // In km + var polygon = circleToPolygon(center, radius) + global_data.last_circle_radius = radius / 1000 // Convert to km to match what UI shows + global_data.last_circle_polygon = polygon + } + return onLayerClick(e) +} - if (type === "circle") { - var center: [number, number] = [layer._latlng.lng, layer._latlng.lat]; - var radius = layer.options.radius; // In km - var polygon = circleToPolygon(center, radius); - global_data.last_circle_radius = radius / 1000; // Convert to km to match what UI shows - global_data.last_circle_polygon = polygon; - } - return onLayerClick(e); +function onLayerClick(e: any) { + const global_data = window.__GLOBAL_DATA__ + global_data.last_object_clicked = e.latlng + let details: Array = [] + if (e.layer && e.layer.toGeoJSON) { + global_data.last_active_drawing = e.layer.toGeoJSON() + } + if (global_data.drawn_items.toGeoJSON) { + details = global_data.drawn_items.toGeoJSON().features } + global_data.all_drawings = details + debouncedUpdateComponentValue(window.map) +} - function onLayerClick(e: any) { - const global_data = __GLOBAL_DATA__; - global_data.last_object_clicked = e.latlng; - let details: Array = []; - if (e.layer && e.layer.toGeoJSON) { - global_data.last_active_drawing = e.layer.toGeoJSON(); - } - if (global_data.drawn_items.toGeoJSON) { - details = global_data.drawn_items.toGeoJSON().features; - } - global_data.all_drawings = details; - debouncedUpdateComponentValue() +window.initComponent = (map: any) => { + map.on("click", onMapClick) + map.on("moveend", onMapMove) + for (let key in map._layers) { + let layer = map._layers[key] + layer.on("click", onLayerClick) } + map.on("draw:created", onDraw) + map.on("draw:edited", onDraw) + map.on("draw:deleted", onDraw) + + Streamlit.setFrameHeight() + updateComponentValue(map) +} - if (map == null) { - try { - map = __GLOBAL_DATA__.map; - } catch (e) { - // Only run this if the map hasn't already been created (and thus the global - //data hasn't been initialized) - const map_div = document.getElementById("map_div"); - const map_div2 = document.getElementById("map_div2"); - if (map_div2) { - map_div2.style.height = `${height}px` - map_div2.style.width = `${width}px` +/** + * The component's render function. This will be called immediately after + * the component is initially loaded, and then again every time the + * component gets new data from Python. + */ +function onRender(event: Event): void { + // Get the RenderData from the event + const data = (event as CustomEvent).detail + + const fig: string = data.args["fig"] + const height: number = data.args["height"] + const width: number = data.args["width"] + + if (!window.map) { + // Only run this if the map hasn't already been created (and thus the global + //data hasn't been initialized) + const div1 = document.getElementById("map_div") + const div2 = document.getElementById("map_div2") + if (div2) { + div2.style.height = `${height}px` + div2.style.width = `${width}px` + } + if (div1) { + div1.style.height = `${height}px` + div1.style.width = `${width}px` + + if (fig.indexOf("document.getElementById('export')") !== -1) { + let a = document.createElement("a") + a.href = "#" + a.id = "export" + a.innerHTML = "Export" + document.body.appendChild(a) } - if (map_div) { - map_div.style.height = `${height}px` - map_div.style.width = `${width}px` - - if (fig.indexOf("document.getElementById('export')") !== -1) { - let a = document.createElement("a"); - a.href = "#"; - a.id = "export"; - a.innerHTML = "Export"; - document.body.appendChild(a); - } - - const render_script = document.createElement("script") - // HACK -- update the folium-generated JS to add, most importantly, - // the map to this global variable so that it can be used elsewhere - // in the script. - let set_global_data = ` - window.__GLOBAL_DATA__ = { - map: map_div, - bounds: map_div.getBounds(), - lat_lng_clicked: null, - last_object_clicked: null, - all_drawings: null, - last_active_drawing: null, - zoom: null, - drawn_items: [], - last_circle_radius: null, - last_circle_polygon: null, - };`; - let replaced = fig + set_global_data; - render_script.innerHTML = replaced; - document.body.appendChild(render_script); - - const global_data = __GLOBAL_DATA__; - let map = global_data.map; - - map.on('click', onMapClick); - map.on('moveend', onMapMove); - for (let key in map._layers) { - let layer = map._layers[key]; - layer.on("click", onLayerClick) - } - map.on('draw:created', onDraw); - map.on('draw:edited', onDraw); - map.on('draw:deleted', onDraw); - - Streamlit.setFrameHeight() - updateComponentValue(); + + const render_script = document.createElement("script") + // HACK -- update the folium-generated JS to add, most importantly, + // the map to this global variable so that it can be used elsewhere + // in the script. + + window.__GLOBAL_DATA__ = { + lat_lng_clicked: null, + last_object_clicked: null, + all_drawings: null, + last_active_drawing: null, + zoom: null, + drawn_items: [], + last_circle_radius: null, + last_circle_polygon: null, } + // The folium-generated script creates a variable called "map_div", which + // is the actual Leaflet map. + render_script.innerHTML = + fig + `window.map = map_div; window.initComponent(map_div);` + document.body.appendChild(render_script) } } } @@ -165,4 +161,4 @@ Streamlit.setComponentReady() // Finally, tell Streamlit to update our initial height. We omit the // `height` parameter here to have it default to our scrollHeight. -Streamlit.setFrameHeight() \ No newline at end of file +Streamlit.setFrameHeight()