Drawing Tools¶
Bokeh's drawing tools are the basis for a wide range of functionality in EarthSim, using the convenient interface provided by HoloViews. They make it simple to build systems for annotating existing data, highlighting regions of interest, and drawing and editing shapes that can be used as input to simulators or other programs. This user guide will give a basic introduction to the drawing tools, including how to access the resulting data from within Python code.
For more detail about the underlying Bokeh tools, see the Bokeh user guide. Note that most of the discussion here is not specific to EarthSim, and applies to any usage of the drawing tools in practice, apart from a few I/O routines imported from earthsim
when used below.
import os
import numpy as np
import holoviews as hv
import geoviews as gv
import cartopy.crs as ccrs
from holoviews import opts
from holoviews.streams import PointDraw, PolyEdit, BoxEdit, PolyDraw, FreehandDraw
hv.extension('bokeh')
tiles = gv.tile_sources.Wikipedia.opts(width=900, height=500)
Drawing Points¶
All drawing tools are added by instantiating a corresponding HoloViews stream, which also syncs the data. Here we will use the PointDraw
stream, which allows adding points, dragging points, and deleting points. The num_objects
parameter, if set, will limit the number of points that can be drawn, ensuring that when the limit is reached the oldest point is dropped.
Add point: Tap anywhere on the plot; each tap adds one point.
Move point: Tap and drag an existing point, which will be dropped once you let go of the mouse button.
Delete point: Tap a point to select it, then press the Backspace key (sometimes labeled "Delete") while the mouse is within the plot area.
Note that to use the PointDraw
tool or any of the other drawing tools, you first need to select the icon for it in the toolbar:
points = gv.Points(np.random.rand(10, 2)*10)
point_stream = PointDraw(source=points, num_objects=10)
(tiles * points).opts(
opts.Points(tools=['hover'], size=10, color='red'))
Note that here and in the other examples below, we have provided initial values for the source
, just so that there will be objects in the map when this notebook is rendered as a web page or otherwise shared. In practice, the source
here and in every case below can be an empty list []
if you don't want any initial values.
Once points are available on the map, we can wrap them in a GeoViews Points object, project them back into longitude and latitude, and then convert the resulting object to a dataframe for use in any Python code:
if point_stream.data:
display(point_stream.element.dframe())
Of course, the dataframe output above will only contain the points that were present at the time that cell was executed, so the cell will need to be re-run if you add points to the main plot.
Drawing bounding boxes¶
The BoxEdit
stream adds a tool that allows drawing, dragging, and deleting rectangular bounding boxes, once you have selected it in the toolbar:
The num_objects
parameter, if set, will limit the number of boxes that can be drawn, causing the oldest box to be dropped when the limit is exceeded.
Add box: Hold shift, then click and drag anywhere on the plot.
Move box: Click and drag an existing box; the box will be dropped once you let go of the mouse button.
Delete box: Tap a box to select it, then press the Backspace (or Delete) key while the mouse is within the plot area.
sample_box = hv.Bounds((-90.99, 32.25, -90.85, 32.37))
box_poly = gv.Polygons([sample_box])
box_stream = BoxEdit(source=box_poly, num_objects=3)
(tiles * box_poly).opts(
opts.Polygons(fill_alpha=0, line_color='black', selection_fill_color='red'))
Note that BoxEdit
accepts a Polygon
element, as there is not yet a vectorized Box type that would let it generate boxes directly, and so we will need to convert the returned polygons into boxes manually:
def bbox(poly):
"Convert the polygon returned by the BoxEdit stream into a bounding box tuple"
xs,ys = poly.array().T
return (xs[0], ys[0], xs[2], ys[2])
if box_stream.element:
polygons = box_stream.element.split()
bboxes = [bbox(p) for p in polygons]
print(bboxes)
(Of course, boxes will only be printed above if they were drawn on the map before the cell above is executed.)
Polygon Editing¶
The PolyEdit
stream adds a Bokeh tool to the source plot that allows drawing, dragging, and deleting vertices on polygons and making the drawn data available to Python:
The tool supports the following actions:
Show vertices: Double tap an existing patch or multi-line
Add vertex: Double tap an existing vertex to select it, then the tool will draw the next point; to add it tap in a new location. To finish editing and add a point, double tap; otherwise press the ESC key to cancel.
Move vertex: Drag an existing vertex and let go of the mouse button to release it.
Delete vertex: After selecting one or more vertices press Backspace (or Delete) while the mouse cursor is within the plot area.
shapefile = '../data/vicksburg_watershed/watershed_boundary.shp'
mask_poly = gv.Shape.from_shapefile(shapefile)
vertex_stream = PolyEdit(source=mask_poly)
tiles * mask_poly.opts(tools=['box_select'], alpha=0.5, line_width=3)
Once the shape has been edited, it can be pulled out into its own file for later usage, and displayed separately:
from earthsim.io import save_shapefile
if vertex_stream.data:
edited_shape_fname = '../data/vicksburg_watershed_edited/watershed_boundary.shp'
dir_name = os.path.dirname(edited_shape_fname)
if not os.path.isdir(dir_name): os.makedirs(dir_name)
save_shapefile(vertex_stream.data, edited_shape_fname, shapefile)
mask_shape = gv.Shape.from_shapefile(edited_shape_fname)
mask_shape = mask_shape.opts() # Clear options to avoid adding edit tool
mask_shape.opts(width=600, height=400, alpha=0.5)
Freehand Drawing¶
The FreehandDraw
tool allows drawing polygons or paths (polylines), depending on whether it is given a Path or Polygon source, using simple click and drag actions:
The num_objects
parameter, if set, will limit the number of lines/polygons that can be drawn, causing the oldest object to be dropped when the limit is exceeded.
Add patch/multi-line: Click and drag to draw a line or polygon and release mouse to finish drawing
Delete patch/multi-line: Tap a patch/multi-line to select it, then press Backspace/Delete while the mouse is within the plot area.
path = gv.Path([[(0, 52), (-74, 43)]])
freehand_stream = FreehandDraw(source=path, num_objects=3)
tiles * path.opts(line_width=5, color='black')
freehand_stream.element.data
Drawing Polygons¶
The PolyDraw
tool allows drawing new polygons or paths (polylines) on a plot, depending on whether it is given a Path or Polygon source:
The num_objects
parameter, if set, will limit the number of lines/polygons that can be drawn, causing the oldest object to be dropped when the limit is exceeded. Additionally it is possible to display and snap to existing vertices by enabling the show_vertices
parameter.
Add patch/multi-line: Double tap to add the first vertex, then use tap to add each subsequent vertex. To finalize the draw action, double tap to insert the final vertex or press the ESC key to stop drawing.
Move patch/multi-line: Tap and drag an existing patch/polyline; the point will be dropped once you let go of the mouse button.
Delete patch/multi-line: Tap a patch/multi-line to select it, then press Backspace/Delete while the mouse is within the plot area.
sample_poly=dict(
Longitude = [-90.86, -90.94, -91.0 , -90.92, -91.0 , -90.94],
Latitude = [ 32.33, 32.37, 32.34, 32.32, 32.27, 32.25])
sample_path=dict(
Longitude = [-90.99, -90.90, -90.90, -90.98],
Latitude = [ 32.35, 32.34, 32.32, 32.25])
new_polys = gv.Polygons([sample_poly])
new_paths = gv.Path([sample_path])
poly_stream = PolyDraw(source=new_polys, show_vertices=True)
path_stream = PolyDraw(source=new_paths, show_vertices=True)
(tiles * new_polys * new_paths).opts(
opts.Path(line_width=5, color='black'),
opts.Polygons(fill_alpha=0.1, line_color='black'))
path_stream.element.data
Notice that the toolbar has two PolyDraw
tools here; if you select the first one you'll be able to add Polygons
(drawn with thin lines), and if you select the other one you can add Path
objects (poly-lines, drawn with a thick line). You'll need to have the appropriate copy of the tool selected if you want to move or delete an object associated with that stream.
Once you have drawn some objects, you can extract the new paths or polygons from the stream (which will be blank unless you have drawn something above when the following cells are executed):
poly_stream.element.geom()
path_stream.element.geom()
Here .geom()
returns a Shapely geometry with all of the shapes you drew of that type. If you would rather work with each shape separately, you can get them as a list with poly_stream.element.split()
or path_stream.element.split()
.
Drawing and editing a polygon¶
By adding tools for both polygon drawing and vertex editing on the same HoloViews object, we can both draw and edit polygons in the same plot:
new_polys = gv.Polygons([sample_poly])
poly_stream = PolyDraw(source=new_polys)
vertex_stream = PolyEdit(source=new_polys)
tiles * new_polys.opts(fill_alpha=0.2, line_color='black')
poly_stream.data
poly_stream.element
The above examples should make it clear how to draw shapes and use the data from within Python. The next set of examples show how to associate data interactively with each point or object added, via Annotators.