S
Solara4mo ago
conic

Help: Drawing a rectangle on an ipyleaflet map

Trying to create an ipyleaflet component but the state and view management is a bit over my head. Does anyone have experience in creating a component that updates a "bounding_boxes" reactive element?
32 Replies
conic
conic4mo ago
An interactive example would be the box drawing tool on https://geojson.io
geojson.io
geojson.io | powered by Mapbox
A quick, simple tool for creating, viewing, and sharing spatial data.
conic
conic4mo ago
No description
conic
conic4mo ago
Unless there's precedent in solara, I think it may be easier to do this in html/javascript for the purposes of my project.
conic
conic4mo ago
Not exactly. I know that there's something that lets one draw squares or polygons Im going to look around for it Ah ok, I think this might be it https://ipyleaflet.readthedocs.io/en/latest/controls/draw_control.html#ipyleaflet.leaflet.DrawControl.on_draw Draw Control
conic
conic4mo ago
No description
conic
conic4mo ago
It creates a menu like this for drawing various shapes Upon finishing a shape, It would be cool if there were a reactive variable that is a list of these shapes/geometries.
Stefan
Stefan4mo ago
You can use the on_draw callback to write to a reactive variable
conic
conic4mo ago
I've considered this, I think my main issue is that I don't understand how Im supposed to render a map once I've created it. Here's what I have so far:
@solara.component
def Map():

def foobar(*args, **kwargs):
print("foobar")


draw_control = DrawControl(on_draw = foobar)
draw_control.polyline = {}
draw_control.polygon = {}
draw_control.circle = {}

draw_control.rectangle = {
"shapeOptions": {
"fillColor": "#fca45d",
"color": "#fca45d",
"fillOpacity": 1.0
}
}

map = ipyleaflet.Map()
map.add_control(draw_control)
map.element()

@solara.component
def Page():
Map()
@solara.component
def Map():

def foobar(*args, **kwargs):
print("foobar")


draw_control = DrawControl(on_draw = foobar)
draw_control.polyline = {}
draw_control.polygon = {}
draw_control.circle = {}

draw_control.rectangle = {
"shapeOptions": {
"fillColor": "#fca45d",
"color": "#fca45d",
"fillOpacity": 1.0
}
}

map = ipyleaflet.Map()
map.add_control(draw_control)
map.element()

@solara.component
def Page():
Map()
calling element is what seems to make anything appear.
conic
conic4mo ago
however it displays a map with now draw control menu:
No description
conic
conic4mo ago
every example of the ipyleaflet documentation site mentions that I can only add a draw control to a map object.
Monty Python
Monty Python4mo ago
pvcastfrontend/02-config.py line 1103
solara.display(lf_map)
solara.display(lf_map)
Stefan
Stefan4mo ago
this is what I do
conic
conic4mo ago
oh great!
Stefan
Stefan4mo ago
solara documentation mentions they want you to use .element on every object, I didn't do that so this is probably not best practice
conic
conic4mo ago
I would too, but it hasn't been working out for me. I do like the ability to make explicity calls to display. Thank you for the guidance and answer Stefan @Stefan
cr33pyguy
cr33pyguy4mo ago
Sorry to join the party so late I don't know if you already got it working, but something like this also works:
ipyleaflet.Map.element( # type: ignore
zoom=zoom.value,
on_zoom=zoom.set,
center=center.value,
on_center=center.set,
scroll_wheel_zoom=True,
layers=[ipyleaflet.TileLayer.element(url=map_layer)],
controls=[draw_control, ipyleaflet.FullScreenControl()],
)
ipyleaflet.Map.element( # type: ignore
zoom=zoom.value,
on_zoom=zoom.set,
center=center.value,
on_center=center.set,
scroll_wheel_zoom=True,
layers=[ipyleaflet.TileLayer.element(url=map_layer)],
controls=[draw_control, ipyleaflet.FullScreenControl()],
)
where draw_control can hook up to the data using on_draw and ipyleaflet.DrawControl.data
conic
conic4mo ago
I didn't really get it working. Im running into new issues: 1) the on_draw callback doesn't seem to be called when I draw the code for that looks like this
@solara.component
def Map():

def foobar(*args, **kwargs):
print("foobar")

draw_control = DrawControl(on_draw = foobar)
draw_control.polyline = {}
draw_control.polygon = {}
draw_control.circle = {}
draw_control.rectangle = {
"shapeOptions": {
"fillColor": "#fca45d",
"color": "#fca45d",
"fillOpacity": 0.4
}
}


ipyleaflet.Map.element( # type: ignore
zoom=zoom_level.value,
on_zoom=zoom_level.set,
center=center.value,
on_center=center.set,
scroll_wheel_zoom=True,
controls=[draw_control, ipyleaflet.FullScreenControl(), ipyleaflet.ZoomControl()],
)

@solara.component
def Page():
Map()
@solara.component
def Map():

def foobar(*args, **kwargs):
print("foobar")

draw_control = DrawControl(on_draw = foobar)
draw_control.polyline = {}
draw_control.polygon = {}
draw_control.circle = {}
draw_control.rectangle = {
"shapeOptions": {
"fillColor": "#fca45d",
"color": "#fca45d",
"fillOpacity": 0.4
}
}


ipyleaflet.Map.element( # type: ignore
zoom=zoom_level.value,
on_zoom=zoom_level.set,
center=center.value,
on_center=center.set,
scroll_wheel_zoom=True,
controls=[draw_control, ipyleaflet.FullScreenControl(), ipyleaflet.ZoomControl()],
)

@solara.component
def Page():
Map()
2) The polygons drawn on the map disappear when I pan or zoom
conic
conic4mo ago
cr33pyguy
cr33pyguy4mo ago
I'll take a look at that tomorrow, I can't see any immediate reason why that would be You should store the shapes into a reactive war data_reactive and then bind that to the data attribute of the DrawControl
conic
conic4mo ago
I'll give this a try, thanks perhaps it rerenders the map each time or resets the data attribute you mentioned. binding a reactive variable to the data attribute might solve both of these, giving it a try I tried binding it, it did not work. I need to look more into what the binding would look liked (expressed in code). The solara documentation is pretty sparse which makes me believe that there's another library or paradigm at play that's obvious to seasoned users, but hidden from me.
cr33pyguy
cr33pyguy4mo ago
Yeah, this is essentially how it works, with rerendering on map view change Interesting. I don't have my code, but if I remember correctly
draw_control = DrawControl(data=data_reactive)
draw_control = DrawControl(data=data_reactive)
worked for me (should be noted I am working with a custom implementation of the controls, although they should be identical in this respect)
conic
conic4mo ago
I've tried this but it throws errors. I know that data IS a field, but Im not sure how to instantiate the solara reactive for it.
What I've tried:
data_reactive = solara.reactive([])
data_reactive = solara.reactive([])
data_reactive = solara.reactive({})
data_reactive = solara.reactive({})
The error
traitlets.traitlets.TraitError: The 'data' trait of a DrawControl instance expected a list, not the Reactive <Reactive value=[{'type': 'Polygon', 'coordinates': [[[-126.914063, 17.644022], [-126.914063, 65.219894], [-8.085938, 65.219894], [-8.085938, 17.644022], [-126.914063, 17.644022]]]}, {'type': 'Polygon', 'coordinates': [[[-126.210938, -7.710992], [-126.210938, 57.891497], [49.570313, 57.891497], [49.570313, -7.710992], [-126.210938, -7.710992]]]}, {'type': 'Polygon', 'coordinates': [[[-138.164063, 8.407168], [-138.164063, 57.136239], [-56.601563, 57.136239], [-56.601563, 8.407168], [-138.164063, 8.407168]]]}, {'type': 'Polygon', 'coordinates': [[[70.664063, 38.822591], [70.664063, 59.175928], [109.335938, 59.175928], [109.335938, 38.822591], [70.664063, 38.822591]]]}, {'type': 'Polygon', 'coordinates': [[[-112.851563, 15.284185], [-112.851563, 63.391522], [2.8125, 63.391522], [2.8125, 15.284185], [-112.851563, 15.284185]]]}, {'type': 'Polygon', 'coordinates': [[[-134.296875, -4.565474], [-134.296875, 60.239811], [-11.601563, 60.239811], [-11.601563, -4.565474], [-134.296875, -4.565474]]]}] id=0x13c1174d0>.
traitlets.traitlets.TraitError: The 'data' trait of a DrawControl instance expected a list, not the Reactive <Reactive value=[{'type': 'Polygon', 'coordinates': [[[-126.914063, 17.644022], [-126.914063, 65.219894], [-8.085938, 65.219894], [-8.085938, 17.644022], [-126.914063, 17.644022]]]}, {'type': 'Polygon', 'coordinates': [[[-126.210938, -7.710992], [-126.210938, 57.891497], [49.570313, 57.891497], [49.570313, -7.710992], [-126.210938, -7.710992]]]}, {'type': 'Polygon', 'coordinates': [[[-138.164063, 8.407168], [-138.164063, 57.136239], [-56.601563, 57.136239], [-56.601563, 8.407168], [-138.164063, 8.407168]]]}, {'type': 'Polygon', 'coordinates': [[[70.664063, 38.822591], [70.664063, 59.175928], [109.335938, 59.175928], [109.335938, 38.822591], [70.664063, 38.822591]]]}, {'type': 'Polygon', 'coordinates': [[[-112.851563, 15.284185], [-112.851563, 63.391522], [2.8125, 63.391522], [2.8125, 15.284185], [-112.851563, 15.284185]]]}, {'type': 'Polygon', 'coordinates': [[[-134.296875, -4.565474], [-134.296875, 60.239811], [-11.601563, 60.239811], [-11.601563, -4.565474], [-134.296875, -4.565474]]]}] id=0x13c1174d0>.
cr33pyguy
cr33pyguy4mo ago
Ah My bad, made a rookie mistake in that code. It should of course be data_reactive.value Where the value is a list And then data reactive should be set by the on_draw callback
conic
conic4mo ago
I see, so .value is a mutable reference that the reactive element would still track? Giving this a try @Iisakki Rotko that works! here are the caveats there is a drawControl buffer (objects that were drawn), those are always wiped when you update the map. as long as you update the reactive element with your new drawing using on_draw , what you've drawn will be erased.
data_reactive = solara.reactive([])

@solara.component
def Map():
def on_draw_rectangle(*args, **kwargs):
data_reactive.value.append(kwargs["geo_json"]["geometry"])

test = List().tag(sync=True)

for x in data_reactive.value:
print(x)

draw_control = DrawControl(data = data_reactive.value)
draw_control.polyline = {}
draw_control.polygon = {}
draw_control.circle = {}
draw_control.rectangle = {
"shapeOptions": {
"fillColor": "#fca45d",
"color": "#fca45d",
"fillOpacity": 0.4,
}
}
draw_control.on_draw(on_draw_rectangle)


ipyleaflet.Map.element( # type: ignore
zoom=zoom_level.value,
on_zoom=zoom_level.set,
center=center.value,
on_center=center.set,
scroll_wheel_zoom=True,
controls=[draw_control,
ipyleaflet.FullScreenControl(),
ipyleaflet.ZoomControl()],
)

@solara.component
def Page():
Map()
data_reactive = solara.reactive([])

@solara.component
def Map():
def on_draw_rectangle(*args, **kwargs):
data_reactive.value.append(kwargs["geo_json"]["geometry"])

test = List().tag(sync=True)

for x in data_reactive.value:
print(x)

draw_control = DrawControl(data = data_reactive.value)
draw_control.polyline = {}
draw_control.polygon = {}
draw_control.circle = {}
draw_control.rectangle = {
"shapeOptions": {
"fillColor": "#fca45d",
"color": "#fca45d",
"fillOpacity": 0.4,
}
}
draw_control.on_draw(on_draw_rectangle)


ipyleaflet.Map.element( # type: ignore
zoom=zoom_level.value,
on_zoom=zoom_level.set,
center=center.value,
on_center=center.set,
scroll_wheel_zoom=True,
controls=[draw_control,
ipyleaflet.FullScreenControl(),
ipyleaflet.ZoomControl()],
)

@solara.component
def Page():
Map()
cr33pyguy
cr33pyguy4mo ago
Would something like
data_reactive.value = [*data_reactive.value, new_data]
data_reactive.value = [*data_reactive.value, new_data]
work?
conic
conic4mo ago
yeah that's what I do on the callback essentially
cr33pyguy
cr33pyguy4mo ago
Yeah, I was too quick to reply
conic
conic4mo ago
thank you for your help by the way, I don't think I'd have gotten this far. Now I just need to find an on_delete. but that should be simpler now that I kind of understand how to persist state to a reactive variable.
cr33pyguy
cr33pyguy4mo ago
IIRC there are some issues with using .append() on reactive values, so maybe try the code I sent, just in case No worries, glad to help!