{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Bokeh's drawing tools are the basis for a wide range of functionality in EarthSim, using the convenient interface provided by [HoloViews](http://holoviews.org). 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.\n", "\n", "For more detail about the underlying Bokeh tools, see the [Bokeh user guide](https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#userguide-tools-edit). 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.\n", "\n", "" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import numpy as np\n", "import holoviews as hv\n", "import geoviews as gv\n", "import cartopy.crs as ccrs\n", "\n", "from holoviews import opts\n", "from holoviews.streams import PointDraw, PolyEdit, BoxEdit, PolyDraw, FreehandDraw\n", "\n", "hv.extension('bokeh')\n", "\n", "tiles = gv.tile_sources.Wikipedia.opts(width=900, height=500)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Drawing Points\n", "\n", "All drawing tools are added by instantiating a corresponding [HoloViews stream](http://holoviews.org/user_guide/Responding_to_Events.html), 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.\n", "\n", "**Add point**: Tap anywhere on the plot; each tap adds one point.\n", "\n", "**Move point**: Tap and drag an existing point, which will be dropped once you let go of the mouse button.\n", "\n", "**Delete point**: Tap a point to select it, then press the Backspace key (sometimes labeled \"Delete\") while the mouse is within the plot area.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "points = gv.Points(np.random.rand(10, 2)*10)\n", "point_stream = PointDraw(source=points, num_objects=10)\n", "\n", "(tiles * points).opts(\n", " opts.Points(tools=['hover'], size=10, color='red'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "if point_stream.data:\n", " display(point_stream.element.dframe())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "## Drawing bounding boxes\n", "\n", "The ``BoxEdit`` stream adds a tool that allows drawing, dragging, and deleting rectangular bounding boxes, once you have selected it in the toolbar: \n", "\n", "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.\n", "\n", "**Add box**: Hold shift, then click and drag anywhere on the plot.\n", "\n", "**Move box**: Click and drag an existing box; the box will be dropped once you let go of the mouse button.\n", "\n", "**Delete box**: Tap a box to select it, then press the Backspace (or Delete) key while the mouse is within the plot area." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_box = hv.Bounds((-90.99, 32.25, -90.85, 32.37))\n", "\n", "box_poly = gv.Polygons([sample_box])\n", "box_stream = BoxEdit(source=box_poly, num_objects=3)\n", "(tiles * box_poly).opts(\n", " opts.Polygons(fill_alpha=0, line_color='black', selection_fill_color='red'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def bbox(poly):\n", " \"Convert the polygon returned by the BoxEdit stream into a bounding box tuple\"\n", " xs,ys = poly.array().T\n", " return (xs[0], ys[0], xs[2], ys[2])\n", "\n", "if box_stream.element:\n", " polygons = box_stream.element.split()\n", " bboxes = [bbox(p) for p in polygons]\n", " print(bboxes)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(Of course, boxes will only be printed above if they were drawn on the map before the cell above is executed.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Polygon Editing\n", "\n", "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:\n", "\n", "The tool supports the following actions:\n", "\n", "**Show vertices**: Double tap an existing patch or multi-line\n", "\n", "**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.\n", "\n", "**Move vertex**: Drag an existing vertex and let go of the mouse button to release it.\n", "\n", "**Delete vertex**: After selecting one or more vertices press Backspace (or Delete) while the mouse cursor is within the plot area." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "shapefile = '../data/vicksburg_watershed/watershed_boundary.shp'\n", "\n", "mask_poly = gv.Shape.from_shapefile(shapefile)\n", "vertex_stream = PolyEdit(source=mask_poly)\n", "\n", "tiles * mask_poly.opts(tools=['box_select'], alpha=0.5, line_width=3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once the shape has been edited, it can be pulled out into its own file for later usage, and displayed separately:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from earthsim.io import save_shapefile\n", "if vertex_stream.data:\n", " edited_shape_fname = '../data/vicksburg_watershed_edited/watershed_boundary.shp'\n", " dir_name = os.path.dirname(edited_shape_fname)\n", " if not os.path.isdir(dir_name): os.makedirs(dir_name)\n", " save_shapefile(vertex_stream.data, edited_shape_fname, shapefile)\n", " mask_shape = gv.Shape.from_shapefile(edited_shape_fname)\n", "mask_shape = mask_shape.opts() # Clear options to avoid adding edit tool\n", "\n", "mask_shape.opts(width=600, height=400, alpha=0.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Freehand Drawing\n", "\n", "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:\n", "\n", "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.\n", "\n", "**Add patch/multi-line**: Click and drag to draw a line or polygon and release mouse to finish drawing\n", "\n", "**Delete patch/multi-line**: Tap a patch/multi-line to select it, then press Backspace/Delete while the mouse is within the plot area.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "path = gv.Path([[(0, 52), (-74, 43)]])\n", "freehand_stream = FreehandDraw(source=path, num_objects=3)\n", "\n", "tiles * path.opts(line_width=5, color='black')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "freehand_stream.element.data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Drawing Polygons\n", "\n", "The ``PolyDraw`` tool allows drawing new polygons or paths (polylines) on a plot, depending on whether it is given a Path or Polygon source:\n", "\n", "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.\n", "\n", "**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.\n", "\n", "**Move patch/multi-line**: Tap and drag an existing patch/polyline; the point will be dropped once you let go of the mouse button.\n", "\n", "**Delete patch/multi-line**: Tap a patch/multi-line to select it, then press Backspace/Delete while the mouse is within the plot area." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sample_poly=dict(\n", " Longitude = [-90.86, -90.94, -91.0 , -90.92, -91.0 , -90.94],\n", " Latitude = [ 32.33, 32.37, 32.34, 32.32, 32.27, 32.25])\n", "sample_path=dict(\n", " Longitude = [-90.99, -90.90, -90.90, -90.98],\n", " Latitude = [ 32.35, 32.34, 32.32, 32.25])\n", "\n", "new_polys = gv.Polygons([sample_poly])\n", "new_paths = gv.Path([sample_path])\n", "poly_stream = PolyDraw(source=new_polys, show_vertices=True)\n", "path_stream = PolyDraw(source=new_paths, show_vertices=True)\n", "\n", "(tiles * new_polys * new_paths).opts(\n", " opts.Path(line_width=5, color='black'),\n", " opts.Polygons(fill_alpha=0.1, line_color='black'))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "path_stream.element.data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "poly_stream.element.geom()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "path_stream.element.geom()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here `.geom()` returns a [Shapely geometry](https://toblerity.org/shapely/shapely.geometry.html) 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()`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Drawing and editing a polygon\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new_polys = gv.Polygons([sample_poly])\n", "poly_stream = PolyDraw(source=new_polys)\n", "vertex_stream = PolyEdit(source=new_polys)\n", "\n", "tiles * new_polys.opts(fill_alpha=0.2, line_color='black')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "poly_stream.data" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "poly_stream.element" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](Annotators.ipynb)." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 2 }