Skip to content

Commit

Permalink
Fix google earth engine flickering and cleanup code (#66)
Browse files Browse the repository at this point in the history
* WIP

* Cleaned up TS (#65)

* Make map key not depend on random google earth url, and get map populated

* Cleanup and bump version

* Remove print

* Rename and add comments for clarity

* Format

Co-authored-by: Ken McGrady <[email protected]>
  • Loading branch information
blackary and kmcgrady authored May 23, 2022
1 parent dfbc443 commit 4c5f69a
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 140 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setuptools.setup(
name="streamlit_folium",
version="0.6.10",
version="0.6.11",
author="Randy Zwitch",
author_email="[email protected]",
description="Render Folium objects in Streamlit",
Expand Down
5 changes: 5 additions & 0 deletions streamlit_folium/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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/<random_hash>, 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

Expand Down
274 changes: 135 additions & 139 deletions streamlit_folium/frontend/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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<RenderData>).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<any> = []
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<any> = [];
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<RenderData>).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)
}
}
}
Expand All @@ -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()
Streamlit.setFrameHeight()

0 comments on commit 4c5f69a

Please sign in to comment.