Add New Polygon to Dash Leaflet Map via a Callback

Im very new to working with GIS data (using Dash Leaflet and GeoPandas) and am currently stumped.

My goal is to create a simple app which does the following:

  • App starts with an empty dash_leaflet.Map() figure and a numeric input box titled “Buffer Distance” (with a default of 100)
  • User draws a polygon on the map which fires a callback
  • Callback takes in the GeoJSON data from the map and the “buffer distance”
  • Use Geopandas to import the GeoJSON data and create a new polygon which is smaller than the user drawn polygon by “Buffer Distance”
  • Pass these 2 polygons (originally drawn & post processed polygon with buffer) back to the map so that both are now displayed on the map

Im having trouble with the last step of pushing the two polygons back the map via some kind of Output

This is the app i am currently working with:

import pandas as pd
from dash import Dash, dcc, html, Input, Output, State
import dash_leaflet as dl
import geopandas as gpd

lat1, lon1 = 36.215487, -81.674006

app = Dash()

input_details = html.Div([
    html.Div([
        html.Div(['Buffer Distance'], style={'width': '37%', 'display': 'inline-block'}),
        dcc.Input(
            value=100,
            id="buffer-distance",
            type='number',
            placeholder='Required',
        ),
    ]),
])

default_map_children = [
    dl.TileLayer(),
    dl.FeatureGroup([
        dl.EditControl(id="edit_control"),
    ]),
    dl.GeoJSON(id='map-geojsons')
]

map_input_results_tab = html.Div(
    [
        html.H2('Add Shapes to Map an Area of Interest'),
        dl.Map(
            id='leaflet-map',
            style={'width': '100%', 'height': '50vh'},
            center=[lat1, lon1],
            zoom=16,
            children=default_map_children
        )
    ])

app.layout = html.Div([input_details, map_input_results_tab])


@app.callback(
    Output('map-geojsons', 'data'),
    Input('edit_control', 'geojson'),
    State('buffer-distance', 'value'),
)
def update_estimates(drawn_geojson, perim_clear):
    if any([x is None for x in [drawn_geojson, perim_clear]]):
        # some value has not been provided, so do not continue with calculations
        return drawn_geojson
    elif not drawn_geojson["features"]:
        # some value has not been provided, so do not continue with calculations
        return drawn_geojson

    gdf = gpd.GeoDataFrame.from_features(drawn_geojson["features"])  # extract user drawn geometry data from UI
    gdf = gdf.set_crs(crs=4326)  # Set the initial CRS to specify that this is lat/lon data
    gdf = gdf.to_crs(
        crs=gdf.estimate_utm_crs())  # Let GeoPandas estimate the best CRS and use that for the area calculation

    # create a new geodataframe using buffer that incorporates the perimeter
    gdf_minus_perim_buffer = gdf['geometry'].buffer(-perim_clear)
    combine_gdf = pd.concat([gdf['geometry'], gdf_minus_perim_buffer])
    # convert back to lat, long
    combine_gdf = combine_gdf.to_crs(crs=4326)
    # convert back to GeoJSON to be rendered in the dash leaflet map
    return_geojson_data = combine_gdf.to_json()

    return return_geojson_data


if __name__ == '__main__':
    app.run_server(debug=True, port=8052)

I think I am close, but am just missing something… Thanks in advance for any help!

P.S. @Emil, sorry to call you out, but i’ve seen you pop up on many helpful threads in my searching :slight_smile:

It looks like the callback approach above is valid, I was just providing the wrong data type back to the dl.GeoJSON’s data attribute .

Changing this line:

# convert back to GeoJSON to be rendered in the dash leaflet map
return_geojson_data = combine_gdf.to_json()

to

# convert back to GeoJSON to be rendered in the dash leaflet map
 return_geojson_data = combine_gdf.__geo_interface__

worked perfectly!

1 Like

You were faster than me :smile:. But for reference, the solution you found is indeed the approach I would recommend :+1:

Thanks, @Emil! Im honestly blown away by the Dash Leaflet library, it’s very cool! I have found it a little difficult to find documentation for how to modify things, but im probably looking in the wrong place. Maybe the problem is that I do not really know anything about Leaflet in JS, so I do not actually know what things I can pass through from Python → Dash Leaflet → Leaflet.

For example, I would like to change the styling of the user drawn Polygons from solid blue line + transparent blue fill to dashed blue line + no fill. Then the subsequent “buffer” polygon would get the solid blue line + transparent blue styling.

Since you’re here, could you please give me a hint on how to do this? I suspect there’s some options attribute that would allow me to set the line color, line dash, and fill transparency for each?

Thanks! Did you look through the interactive documentation?

I believe you are looking for the style entry of the options property. You can pass the polygon styling as a dict, or alternatively a function for more complex use cases,

geojson = dl.GeoJSON(url="/assets/us-states.json",  # url to geojson file
                     options=dict(style=style_handle),  # how to style each polygon
...

The Choropleth map example in the docs demonstrates an advanced usecase.

@Emil Ah, this is great. With this I’m able to define the style for the GeoJSON that is created in my callback, but is there a way to change the default styling of the user drawn polygon that comes from dl.EditControl(id="edit_control")? I do not see an option attribute for dl.EditControl

Or am i stuck with the solid blue line + transparent blue fill for that polygon? If so, it’s not a big deal, but it would be nice to be able to modify those colors and line types.

Thanks for all your help!

Yes, you can pass options via the draw/edit properties. To color polygons red for example, the code would be,

draw=dict(polygon=dict(shapeOptions=dict(color='red', opacity=0.5)))

For reference, here is a small example,

import dash_leaflet as dl
from dash import Dash, html

app = Dash()
app.layout = html.Div([
    dl.Map(children=[
        dl.TileLayer(),
        dl.FeatureGroup(dl.EditControl(
            id="edit_control",
            draw=dict(polygon=dict(shapeOptions=dict(color='red', opacity=0.5)))
        )),
    ], style={'width': '50%', 'height': '50vh'}, id="map"),
])

if __name__ == '__main__':
    app.run_server()
1 Like