Annotators

In [1]:
import panel as pn
import holoviews as hv
import geoviews as gv
import cartopy.crs as ccrs

from earthsim.annotators import (PolyAnnotator, PointAnnotator, PolyAndPointAnnotator,
                                 GeoAnnotator,  PointWidgetAnnotator)
hv.extension('bokeh')

This notebook documents the usage and design of a set of example GeoAnnotator classes from the earthsim module, which make it easy to draw, edit, and annotate polygon, polyline, and point data on top of a map. Each of these GeoAnnotator classes builds on Bokeh Drawing Tools connected to HoloViews drawing-tools streams, providing convenient access to the drawn data from Python.

Important caveat: These classes provide complex functionality that can be useful as is, but because each specific use case for annotations is likely to have different requirements, it is best to think of the classes documented here as templates or starting points for whatever specific functionality you need in your own applications.

GeoAnnotator

A GeoAnnotator allows drawing polygons and points on top of a tile source and syncing the drawn data back to Python. It does this by attaching PointDraw, PolyDraw, and VertexEdit streams to the points and polygons, which in turn add the corresponding Bokeh tools.

For use when this notebook is a static web page, we'll supply an initial sample polygon and set of points, but the polys= and points= arguments can be omitted if you want to start with a blank map. Note that if you are viewing this notebook as a static web page, the various drawing tools should work as usual but anything involving executing Python code will not update, since there is no running Python process in that case.

In [2]:
hv.opts.defaults(hv.opts.Path(line_width=5))

sample_poly=dict(
    Longitude = [-10114986, -10123906, -10130333, -10121522, -10129889, -10122959],
    Latitude  = [  3806790,   3812413,   3807530,   3805407,   3798394,   3796693])
sample_points = dict(
    Longitude = [-10131185, -10131943, -10131766, -10131032],
    Latitude  = [  3805587,   3803182,   3801073,   3799778])

annot = GeoAnnotator(polys=[sample_poly], points=sample_points, path_type=gv.Path)
annot.pprint()
annot.panel()
GeoAnnotator06676
-----------------

  extent: (nan, nan, nan, nan)
  feature_style: {'fill_color': 'blue', 'size': 10}
  height: 500
  node_style: {'fill_color': 'indianred', 'size': 6}
  num_points: None
  num_polys: None
  path_type: <class 'geoviews.element.geo.Path'>
  points: :Points   [Longitude,Latitude]
  polys: :Path   [Longitude,Latitude]
  tile_url: http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png
  width: 900

Out[2]:

In the map above, you can use each of the drawing tools from the toolbar to edit the existing objects, delete them, or add new ones, as described in the Drawing tools guide.

Accessing the stream data

The data drawn in the above plot is automatically synced to Python (as long as the Python kernel is running), and we can easily access it on the two stream classes. We'll first look at the polygon data, which can be accessed in a dynamically updated form if we want (updating automatically whenever the polygons change in the above map, as long as Python is running):

In [3]:
annot.poly_stream.dynamic
Out[3]:

You can use .element instead of .dynamic above if you want a static version that is updated only when that cell is executed.

In most cases, however, you will probably want to get direct access to the data, either in a format matching what is accepted by a Bokeh ColumnDataSource (in Web Mercator coordinates):

In [4]:
annot.poly_stream.data
Out[4]:
{'xs': [array([-10114986., -10123906., -10130333., -10121522., -10129889.,
         -10122959.])],
 'ys': [array([3806790., 3812413., 3807530., 3805407., 3798394., 3796693.])]}

or via .element, which makes it easy to project the data into a more natural coordinate system:

In [5]:
gv.project(annot.poly_stream.element, projection=ccrs.PlateCarree()).dframe()
Out[5]:
Longitude Latitude
0 -90.864465 32.330633
1 -90.944595 32.373304
2 -91.002330 32.336250
3 -90.923179 32.320134
4 -90.998341 32.266880
5 -90.936088 32.253959

The same functionality is also available for the point_stream:

In [6]:
gv.project(annot.point_stream.element, projection=ccrs.PlateCarree()).dframe()
Out[6]:
Longitude Latitude
0 -91.009983 32.321501
1 -91.016793 32.303242
2 -91.015203 32.287227
3 -91.008609 32.277392

PointAnnotator

PointAnnotator is an extension of GeoAnnotator that adds support for annotating the points with the help of a table. Whenever a point is added by tapping on the plot, an entry will appear in the table below the plot allowing you to edit the specified point_columns (which we specify here as a Size to be associated with each point).

After selecting the Point Draw Tool you can tap anywhere to draw points, drag the points around, and delete them with backspace. Whenever a point is added it will appear in the table, and by tapping on the empty 'Size' cells you can enter a value, which will also be synced back to Python. Selecting one or more rows in the table will highlight the corresponding points.

Note that points=sample_points is only needed here because we wanted some initial points to show, e.g. on a static web page; it can be omitted if you want to start with a blank map.

In [7]:
import pandas as pd
sample_points = pd.DataFrame({
    'Longitude': [-10131185, -10131943, -10131766, -10131032],
    'Latitude':  [  3805587,   3803182,   3801073,   3799778],
    'Size':      [        5,        50,       500,      5000]})

annot = PointAnnotator(point_columns=['Size'], points=sample_points)
annot.pprint()
annot.panel()
PointAnnotator07098
-------------------

  extent: (nan, nan, nan, nan)
  feature_style: {'fill_color': 'blue', 'size': 10}
  height: 500
  node_style: {'fill_color': 'indianred', 'size': 6}
  num_points: None
  num_polys: None
  path_type: <class 'geoviews.element.geo.Polygons'>
  point_columns: ['Size']
  points: :Points   [Longitude,Latitude]   (Size)
  polys: :Polygons   [Longitude,Latitude]
  table_height: 150
  table_width: 400
  tile_url: http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png
  width: 900

Out[7]:

Once again we can access the annotated points in Python by looking at the point_stream:

In [8]:
gv.project(annot.point_stream.element, projection=ccrs.PlateCarree()).dframe()
Out[8]:
Longitude Latitude Size
0 -91.009983 32.321501 5
1 -91.016793 32.303242 50
2 -91.015203 32.287227 500
3 -91.008609 32.277392 5000

Additionally we can access the currently selected points using the selected_points property:

In [9]:
annot.selected_points
Out[9]:

PolyAnnotator

The PolyAnnotator works much the same as the PointAnnotator except that it allows us to annotate polygons. As before, whenever a polygon is added (now using the Polygon Draw tool) it will appear in the table below, and selecting a row will highlight the corresponding polygon. You can edit the table to associate a 'Group' value, to add multiple attributes to a polygon you can declare any number ofpoly_columns. The PolyAnnotator also allows annotating the vertices in each polygon by defining vertex_columns. When a polygon is selected using the draw tool the vertices will be shown in the second table and can be edited by tapping on a cell and pressing enter:

In [10]:
annot = PolyAnnotator(poly_columns=['Group'], polys=[sample_poly], vertex_columns=['Weight'])
annot.pprint()
annot.panel()
PolyAnnotator07555
------------------

  extent: (nan, nan, nan, nan)
  feature_style: {'fill_color': 'blue', 'size': 10}
  height: 500
  node_style: {'fill_color': 'indianred', 'size': 6}
  num_points: None
  num_polys: None
  path_type: <class 'geoviews.element.geo.Polygons'>
  points: :Points   [Longitude,Latitude]
  poly_columns: ['Group']
  polys: :Polygons   [Longitude,Latitude]   (Group)
  table_height: 150
  table_width: 400
  tile_url: http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png
  vertex_columns: ['Weight']
  width: 900

Out[10]:

When we inspect the stream data we can see that three columns are made available, the 'xs' and 'ys' containing the vertices of each polygon/path and the Group annotation column. The vertex_columns containing the annotations for each vertex will appear in the data once an edit has been made in the table.

In [11]:
annot.poly_stream.data
Out[11]:
{'xs': [array([-10114986., -10123906., -10130333., -10121522., -10129889.,
         -10122959., -10114986.])],
 'ys': [array([3806790., 3812413., 3807530., 3805407., 3798394., 3796693.,
         3806790.])],
 'Group': array([''], dtype='<U1')}

Just as with the PointAnnotator we can access the currently selected polygons or paths using the selected_polygons property:

In [12]:
annot.selected_polygons
Out[12]:
[]

PolyAndPointAnnotator

The PolyAndPointAnnotator combines the PointAnnotator and PolyAnnotator, showing three tables to add annotations to both the points, the polygons and the polygon vertices.

In [13]:
annot = PolyAndPointAnnotator(
    poly_columns =['Group'], polys=[sample_poly], vertex_columns=['Weight'], 
    point_columns=['Size'],  points=sample_points
)
annot.pprint()
annot.panel()
PolyAndPointAnnotator08093
--------------------------

  extent: (nan, nan, nan, nan)
  feature_style: {'fill_color': 'blue', 'size': 10}
  height: 500
  node_style: {'fill_color': 'indianred', 'size': 6}
  num_points: None
  num_polys: None
  path_type: <class 'geoviews.element.geo.Polygons'>
  point_columns: ['Size']
  points: :Points   [Longitude,Latitude]   (Size)
  poly_columns: ['Group']
  polys: :Polygons   [Longitude,Latitude]   (Group)
  table_height: 150
  table_width: 400
  tile_url: http://c.tile.openstreetmap.org/{Z}/{X}/{Y}.png
  vertex_columns: ['Weight']
  width: 900

Out[13]:
In [14]:
annot.poly_stream.data
Out[14]:
{'xs': [array([-10114986., -10123906., -10130333., -10121522., -10129889.,
         -10122959., -10114986.])],
 'ys': [array([3806790., 3812413., 3807530., 3805407., 3798394., 3796693.,
         3806790.])],
 'Group': array([''], dtype='<U1')}

WidgetAnnotator

The WidgetAnnotator takes a different approach to annotating points. Instead of annotating points by editing the Table directly, it allows adding points to a number of predefined groups. To use it,

  1. Add some points
  2. Select a subset of the points by tapping on them or using the box_select tool
  3. Select a group to assign to the points from the dropdown menu
  4. Click the add button

The indexes of the points assigned to each group can be seen in the table.

In [15]:
annotator = PointWidgetAnnotator(['A', 'B', 'C'], points=sample_points)
annotator.panel()
WARNING:param.PointWidgetAnnotator08692: The Parameterized instance has instance parameters created using new-style param APIs, which are incompatible with .params. Use the new more explicit APIs on the .param accessor to query parameter instances.To query all parameter instances use .param.objects with the option to return either class or instance parameter objects. Alternatively use .param[name] indexing to access a specific parameter object by name.
Out[15]:

We can also view the annotated points separately, if we are running a live Python process:

In [16]:
points = annotator.annotated_points()
(points + points.table()).opts(shared_datasource=True)
Out[16]:

These values can then be used in any subsequent Python code.

As you can see, the GeoAnnotator classes make it straightforward to collect user inputs specifying point and polygon data on maps, including associated values, which makes it possible to design convenient user interfaces to simulators and other code that needs inputs situated in geographic coordinates. The specific GeoAnnotator classes used here are already useful for these tasks, but in practice it is very likely that specific applications will need new annotator classes, which you can create by copying and modifying the code for the example classes above (in earthsim/annotators.py), or by subclassing from one of the existing classes and adding specific behavior you need for a particular application.

The Specifying Meshes user guide shows one such application, for collecting specifications for generating an irregular mesh to cover an area of the map with varying levels of detail for different regions.


Right click to download this notebook from GitHub.