Source code for pysepal.mapping.draw_control
"""Customized drawing controls."""
from copy import deepcopy
from typing import Optional
import geopandas as gpd
import ipyleaflet as ipl
from shapely import geometry as sg
from pysepal import color
[docs]
class DrawControl(ipl.DrawControl):
m: Optional[ipl.Map] = None
"the map on which he drawControl is displayed. It will help control the visibility"
[docs]
def __init__(self, m: ipl.Map, **kwargs) -> None:
"""A custom DrawingControl object to handle edition of features.
Args:
m: the map on which he drawControl is displayed
kwargs: any available arguments from a ipyleaflet.DrawingControl
"""
# set some default parameters
options = {"shapeOptions": {"color": color.info}}
kwargs.setdefault("marker", {})
kwargs.setdefault("circlemarker", {})
kwargs.setdefault("polyline", {})
kwargs.setdefault("rectangle", options)
kwargs.setdefault("circle", options)
kwargs.setdefault("polygon", options)
# save the map in the member of the objects
self.m = m
super().__init__(**kwargs)
[docs]
def show(self) -> None:
"""Show the drawing control on the map. and clear it's content."""
self.clear()
self in self.m.controls or self.m.add(self)
return
[docs]
def hide(self) -> None:
"""Hide the drawing control from the map, and clear it's content."""
self.clear()
self not in self.m.controls or self.m.remove(self)
return
[docs]
def to_json(self) -> dict:
"""Return the content of the DrawControl data.
Returned without the styling properties and using a polygonized representation of circles. The output is fully compatible with __geo_interface__.
Returns:
the json representation of all the geometries draw on the map
"""
features = [self.polygonize(feat) for feat in deepcopy(self.data)]
[feat["properties"].pop("style") for feat in features]
return {"type": "FeatureCollection", "features": features}
[docs]
@staticmethod
def polygonize(geo_json: dict) -> dict:
"""Transform a ipyleaflet circle (a point with a radius) into a GeoJson polygon.
The methods preserves all the geo_json other attributes.
If the geometry is not a circle (don't require polygonisation), do nothing.
Params:
geo_json: the circle geojson
Returns:
the polygonised feature
"""
if "Point" not in geo_json["geometry"]["type"]:
return geo_json
# create shapely point
center = sg.Point(geo_json["geometry"]["coordinates"])
point = gpd.GeoSeries([center], crs=4326)
radius = geo_json["properties"]["style"]["radius"]
circle = point.to_crs(3857).buffer(radius).to_crs(4326)
# insert it in the geo_json
output = geo_json.copy()
output["geometry"] = circle[0].__geo_interface__
return output
[docs]
class GeomanDrawControl(ipl.GeomanDrawControl):
"""Drawing control based on Leaflet-Geoman.
Provides the same API as DrawControl (show/hide/to_json/clear) but uses
the Geoman library which offers additional tools (drag, cut, rotate, text).
Usage:
Replace ``DrawControl`` with ``GeomanDrawControl`` in SepalMap or use directly::
from pysepal.mapping import GeomanDrawControl
dc = GeomanDrawControl(my_map)
my_map.add(dc)
"""
m: Optional[ipl.Map] = None
"the map on which the drawControl is displayed"
[docs]
def __init__(self, m: ipl.Map, **kwargs) -> None:
"""A custom DrawingControl based on Leaflet-Geoman.
Args:
m: the map on which the drawControl is displayed
kwargs: any available arguments from ipyleaflet.GeomanDrawControl
"""
# Geoman uses pathOptions instead of shapeOptions
options = {"pathOptions": {"color": color.info}}
kwargs.setdefault("marker", {})
kwargs.setdefault("circlemarker", {})
kwargs.setdefault("polyline", {})
kwargs.setdefault("rectangle", options)
kwargs.setdefault("circle", options)
kwargs.setdefault("polygon", options)
self.m = m
super().__init__(**kwargs)
[docs]
def show(self) -> None:
"""Show the drawing control on the map and clear its content."""
self.clear()
self in self.m.controls or self.m.add(self)
return
[docs]
def hide(self) -> None:
"""Hide the drawing control from the map and clear its content."""
self.clear()
self not in self.m.controls or self.m.remove(self)
return
[docs]
def to_json(self) -> dict:
"""Return the content of the DrawControl data.
Returns a GeoJSON FeatureCollection without styling properties and with
circles polygonized. Compatible with __geo_interface__.
Returns:
the json representation of all the geometries drawn on the map
"""
features = [self.polygonize(feat) for feat in deepcopy(self.data)]
# Remove style/pathOptions from properties (may or may not exist)
for feat in features:
props = feat.get("properties", {})
props.pop("style", None)
props.pop("pathOptions", None)
return {"type": "FeatureCollection", "features": features}
[docs]
@staticmethod
def polygonize(geo_json: dict) -> dict:
"""Transform a circle (a point with a radius) into a GeoJson polygon.
Handles both Geoman format (radius in properties) and legacy format
(radius in properties.style.radius).
Params:
geo_json: the circle geojson
Returns:
the polygonised feature
"""
if "Point" not in geo_json["geometry"]["type"]:
return geo_json
# Look for radius in multiple locations
props = geo_json.get("properties", {})
radius = None
# Geoman stores radius directly in properties
if "radius" in props:
radius = props["radius"]
# Legacy DrawControl stores it in properties.style.radius
elif isinstance(props.get("style"), dict) and "radius" in props["style"]:
radius = props["style"]["radius"]
if radius is None:
return geo_json
# create shapely point
center = sg.Point(geo_json["geometry"]["coordinates"])
point = gpd.GeoSeries([center], crs=4326)
circle = point.to_crs(3857).buffer(radius).to_crs(4326)
# insert it in the geo_json
output = geo_json.copy()
output["geometry"] = circle[0].__geo_interface__
return output