{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "import holoviews as hv\n", "import geoviews as gv\n", "import cartopy.crs as ccrs\n", "\n", "from earthsim.annotators import (PolyAnnotator, PointAnnotator, PolyAndPointAnnotator,\n", " GeoAnnotator, PointWidgetAnnotator)\n", "hv.extension('bokeh')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](Drawing_Tools.ipynb) connected to [HoloViews drawing-tools streams](Drawing_Tools.ipynb), providing convenient access to the drawn data from Python.\n", "\n", "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.\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# GeoAnnotator\n", "\n", "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.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "hv.opts.defaults(hv.opts.Path(line_width=5))\n", "\n", "sample_poly=dict(\n", " Longitude = [-10114986, -10123906, -10130333, -10121522, -10129889, -10122959],\n", " Latitude = [ 3806790, 3812413, 3807530, 3805407, 3798394, 3796693])\n", "sample_points = dict(\n", " Longitude = [-10131185, -10131943, -10131766, -10131032],\n", " Latitude = [ 3805587, 3803182, 3801073, 3799778])\n", "\n", "annot = GeoAnnotator(polys=[sample_poly], points=sample_points, path_type=gv.Path)\n", "annot.pprint()\n", "annot.panel()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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](Drawing_Tools.ipynb) guide." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Accessing the stream data\n", "\n", "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):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot.poly_stream.dynamic" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can use `.element` instead of `.dynamic` above if you want a static version that is updated only when that cell is executed. \n", "\n", "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):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot.poly_stream.data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or via ``.element``, which makes it easy to project the data into a more natural coordinate system:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gv.project(annot.poly_stream.element, projection=ccrs.PlateCarree()).dframe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The same functionality is also available for the ``point_stream``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gv.project(annot.point_stream.element, projection=ccrs.PlateCarree()).dframe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PointAnnotator\n", "\n", "``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).\n", "\n", "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.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "sample_points = pd.DataFrame({\n", " 'Longitude': [-10131185, -10131943, -10131766, -10131032],\n", " 'Latitude': [ 3805587, 3803182, 3801073, 3799778],\n", " 'Size': [ 5, 50, 500, 5000]})\n", "\n", "annot = PointAnnotator(point_columns=['Size'], points=sample_points)\n", "annot.pprint()\n", "annot.panel()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once again we can access the annotated points in Python by looking at the ``point_stream``:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "gv.project(annot.point_stream.element, projection=ccrs.PlateCarree()).dframe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Additionally we can access the currently selected points using the ``selected_points`` property:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot.selected_points" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PolyAnnotator\n", "\n", "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 of``poly_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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot = PolyAnnotator(poly_columns=['Group'], polys=[sample_poly], vertex_columns=['Weight'])\n", "annot.pprint()\n", "annot.panel()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot.poly_stream.data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just as with the ``PointAnnotator`` we can access the currently selected **polygons** or **paths** using the ``selected_polygons`` property:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot.selected_polygons" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PolyAndPointAnnotator\n", "\n", "The ``PolyAndPointAnnotator`` combines the ``PointAnnotator`` and ``PolyAnnotator``, showing three tables to add annotations to both the points, the polygons and the polygon vertices." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot = PolyAndPointAnnotator(\n", " poly_columns =['Group'], polys=[sample_poly], vertex_columns=['Weight'], \n", " point_columns=['Size'], points=sample_points\n", ")\n", "annot.pprint()\n", "annot.panel()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annot.poly_stream.data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## WidgetAnnotator\n", "\n", "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,\n", "\n", "1. Add some points\n", "2. Select a subset of the points by tapping on them or using the box_select tool\n", "3. Select a group to assign to the points from the dropdown menu\n", "4. Click the add button\n", "\n", "The indexes of the points assigned to each group can be seen in the table." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "annotator = PointWidgetAnnotator(['A', 'B', 'C'], points=sample_points)\n", "annotator.panel()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also view the annotated points separately, if we are running a live Python process:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "points = annotator.annotated_points()\n", "(points + points.table()).opts(shared_datasource=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These values can then be used in any subsequent Python code.\n", "\n", "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.\n", "\n", "The [Specifying Meshes](Specifying_Meshes.ipynb) 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." ] } ], "metadata": { "language_info": { "name": "python", "pygments_lexer": "ipython3" } }, "nbformat": 4, "nbformat_minor": 2 }