Browse Source

jupyter: Add example notebooks (#1787)

* Jupyter version of the basic example.
* grass.jupyter tutorial and description.
* Three notebooks with analytical topics.
* Add missing font parameter to GrassRenderer.
Caitlin H 3 years ago
parent
commit
0663dd6ebf

+ 18 - 18
doc/notebooks/example_notebook.ipynb

@@ -36,7 +36,7 @@
     "\n",
     "\n",
     "# Ask GRASS GIS where its Python packages are.\n",
     "# Ask GRASS GIS where its Python packages are.\n",
     "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
     "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
-    "os.environ['GISBASE'] = gisbase\n",
+    "os.environ[\"GISBASE\"] = gisbase\n",
     "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))\n",
     "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))\n",
     "\n",
     "\n",
     "# Import the GRASS GIS packages we need.\n",
     "# Import the GRASS GIS packages we need.\n",
@@ -50,13 +50,13 @@
     "gs.set_raise_on_error(True)\n",
     "gs.set_raise_on_error(True)\n",
     "gs.set_capture_stderr(True)\n",
     "gs.set_capture_stderr(True)\n",
     "# Simply overwrite existing maps like we overwrite Python variable values.\n",
     "# Simply overwrite existing maps like we overwrite Python variable values.\n",
-    "os.environ['GRASS_OVERWRITE'] = '1'\n",
+    "os.environ[\"GRASS_OVERWRITE\"] = \"1\"\n",
     "# Enable map rendering in a notebook.\n",
     "# Enable map rendering in a notebook.\n",
-    "os.environ['GRASS_FONT'] = 'sans'\n",
+    "os.environ[\"GRASS_FONT\"] = \"sans\"\n",
     "# Set display modules to render into a file (named map.png by default)\n",
     "# Set display modules to render into a file (named map.png by default)\n",
-    "os.environ['GRASS_RENDER_IMMEDIATE'] = 'cairo'\n",
-    "os.environ['GRASS_RENDER_FILE_READ'] = 'TRUE'\n",
-    "os.environ['GRASS_LEGEND_FILE'] = 'legend.txt'"
+    "os.environ[\"GRASS_RENDER_IMMEDIATE\"] = \"cairo\"\n",
+    "os.environ[\"GRASS_RENDER_FILE_READ\"] = \"TRUE\"\n",
+    "os.environ[\"GRASS_LEGEND_FILE\"] = \"legend.txt\""
    ]
    ]
   },
   },
   {
   {
@@ -75,11 +75,11 @@
    "metadata": {},
    "metadata": {},
    "outputs": [],
    "outputs": [],
    "source": [
    "source": [
-    "gs.parse_command('g.region', raster=\"lakes\", flags='pg')\n",
-    "gs.run_command('r.buffer', input=\"lakes\", output=\"lakes_buff\", distances=[60, 120, 240, 500])\n",
-    "gs.run_command('d.erase')\n",
-    "gs.run_command('d.rast', map=\"lakes_buff\")\n",
-    "gs.run_command('d.legend', raster=\"lakes_buff\", range=(2, 5), at=(80, 100, 2, 10))\n",
+    "gs.parse_command(\"g.region\", raster=\"lakes\", flags=\"pg\")\n",
+    "gs.run_command(\"r.buffer\", input=\"lakes\", output=\"lakes_buff\", distances=[60, 120, 240, 500])\n",
+    "gs.run_command(\"d.erase\")\n",
+    "gs.run_command(\"d.rast\", map=\"lakes_buff\")\n",
+    "gs.run_command(\"d.legend\", raster=\"lakes_buff\", range=(2, 5), at=(80, 100, 2, 10))\n",
     "Image(filename=\"map.png\")"
     "Image(filename=\"map.png\")"
    ]
    ]
   },
   },
@@ -100,13 +100,13 @@
    "metadata": {},
    "metadata": {},
    "outputs": [],
    "outputs": [],
    "source": [
    "source": [
-    "gs.run_command('v.buffer', input=\"boundary_state\", output=\"buffer\", distance=-10000)\n",
-    "gs.parse_command('g.region', vector=\"boundary_state\", flags='pg')\n",
-    "gs.run_command('d.erase')  # erase the display before drawing again\n",
+    "gs.run_command(\"v.buffer\", input=\"boundary_state\", output=\"buffer\", distance=-10000)\n",
+    "gs.parse_command(\"g.region\", vector=\"boundary_state\", flags=\"pg\")\n",
+    "gs.run_command(\"d.erase\")  # erase the display before drawing again\n",
     "!rm -f $GRASS_LEGEND_FILE  # and remove the legend file\n",
     "!rm -f $GRASS_LEGEND_FILE  # and remove the legend file\n",
-    "gs.run_command('d.vect', map=\"boundary_state\", fill_color=\"#5A91ED\", legend_label=\"State boundary\")\n",
-    "gs.run_command('d.vect', map=\"buffer\", fill_color=\"#F8766D\", legend_label=\"Inner portion\")\n",
-    "gs.run_command('d.legend.vect', at=(10, 35))\n",
+    "gs.run_command(\"d.vect\", map=\"boundary_state\", fill_color=\"#5A91ED\", legend_label=\"State boundary\")\n",
+    "gs.run_command(\"d.vect\", map=\"buffer\", fill_color=\"#F8766D\", legend_label=\"Inner portion\")\n",
+    "gs.run_command(\"d.legend.vect\", at=(10, 35))\n",
     "Image(filename=\"map.png\")"
     "Image(filename=\"map.png\")"
    ]
    ]
   },
   },
@@ -170,7 +170,7 @@
    "name": "python",
    "name": "python",
    "nbconvert_exporter": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.10"
   }
   }
  },
  },
  "nbformat": 4,
  "nbformat": 4,

+ 179 - 0
doc/notebooks/basic_example_grass_jupyter.ipynb

@@ -0,0 +1,179 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Try GRASS GIS in Jupyter Notebook with Python and grass.jupyter\n",
+    "\n",
+    "[<img src=\"../../man/grass_logo.png\" alt=\"GRASS GIS\" style=\"width:200px;\"/>](https://grass.osgeo.org/)\n",
+    "\n",
+    "This is a quick introduction to *GRASS GIS* in a *Jupyter Notebook* using the `grass.jupyter` package and the *Python* scripting language. The `grass.jupyter` package shortens the launch of *GRASS GIS* in *Jupyter Notebook* and provides several useful classes for creating, displaying and saving *GRASS GIS* maps. This notebook can be directly compared with [basic_example.ipynb](basic_example.ipynb) to see how the package improves the integration of *GRASS GIS* and *Jupyter Notebooks*.\n",
+    "\n",
+    "\n",
+    "The `grass.jupyter` package was written as part of Google Summer of Code in 2021. For more information, visit the [wiki page](https://trac.osgeo.org/grass/wiki/GSoC/2021/JupyterAndGRASS).\n",
+    "\n",
+    "\n",
+    "Examples here are using a sample GRASS GIS dataset for North Carolina, USA. The dataset is included in this environment.\n",
+    "\n",
+    "## Usage\n",
+    "\n",
+    "To run the selected part which is called a cell, hit `Shift + Enter`.\n",
+    "\n",
+    "## Start\n",
+    "\n",
+    "There are several ways to use GRASS GIS. When using Python in a notebook, we usually find GRASS GIS Python packages first, import them, initialize GRASS GIS session, and set several variables useful for using GRASS GIS in a notebook."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Import Python standard library and IPython packages we need.\n",
+    "import os\n",
+    "import subprocess\n",
+    "import sys\n",
+    "\n",
+    "# Ask GRASS GIS where its Python packages are.\n",
+    "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
+    "os.environ[\"GISBASE\"] = gisbase\n",
+    "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))\n",
+    "\n",
+    "# Import the GRASS GIS packages we need.\n",
+    "import grass.script as gs\n",
+    "import grass.jupyter as gj\n",
+    "\n",
+    "# Start GRASS Session\n",
+    "gj.init(\"../../data/grassdata\", \"nc_basic_spm_grass7\", \"user1\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Raster buffer\n",
+    "\n",
+    "Set computational region and create multiple buffers in given distances\n",
+    "around lakes represented as raster:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.parse_command(\"g.region\", raster=\"lakes\", flags=\"pg\")\n",
+    "gs.run_command(\"r.buffer\", input=\"lakes\", output=\"lakes_buff\", distances=[60, 120, 240, 500])\n",
+    "\n",
+    "# Start a GrassRenderer\n",
+    "img = gj.GrassRenderer()\n",
+    "\n",
+    "# Add a raster and vector to the map\n",
+    "img.d_rast(map=\"lakes_buff\")\n",
+    "img.d_legend(raster=\"lakes_buff\", range=(2, 5), at=(80, 100, 2, 10))\n",
+    "\n",
+    "# Display map\n",
+    "img.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Vector buffer\n",
+    "\n",
+    "Create a negative buffer around state boundary represented as a vector.\n",
+    "Vector modules typically don't follow computational region,\n",
+    "but we set it to inform display modules about our area of interest."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.run_command(\"v.buffer\", input=\"boundary_state\", output=\"buffer\", distance=-10000)\n",
+    "gs.parse_command(\"g.region\", vector=\"boundary_state\", flags=\"pg\")\n",
+    "\n",
+    "# Start another GrassRenderer\n",
+    "img2 = gj.GrassRenderer()\n",
+    "\n",
+    "# Add vector layers and legend\n",
+    "img2.d_vect(map=\"boundary_state\", fill_color=\"#5A91ED\", legend_label=\"State boundary\")\n",
+    "img2.d_vect(map=\"buffer\", fill_color=\"#F8766D\", legend_label=\"Inner portion\")\n",
+    "img2.d_legend_vect(at=(10, 35))\n",
+    "\n",
+    "# Display map\n",
+    "img2.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Additional GRASS Information and Tutorials\n",
+    "\n",
+    "To find more information on what one can do with GRASS GIS APIs, check out:\n",
+    "    \n",
+    " - [GRASS GIS Manual](https://grass.osgeo.org/grass-stable/manuals)\n",
+    "    \n",
+    " - [GRASS Python API Manual](https://grass.osgeo.org/grass-stable/manuals/libpython)\n",
+    "\n",
+    "For more Jupyter Notebook GRASS GIS tutorials, visit:\n",
+    " - [Try GRASS GIS online](https://grass.osgeo.org/learn/tryonline/)\n",
+    "\n",
+    "## What else is in the sample North Carolina dataset?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(gs.read_command(\"g.list\", type=\"all\"))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## What other GRASS modules can I try in this notebooks?"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print(gs.read_command(\"g.search.modules\", flags=\"g\"))"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}

+ 228 - 0
doc/notebooks/grass_jupyter.ipynb

@@ -0,0 +1,228 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Improved Integration of GRASS and Jupyter\n",
+    "\n",
+    "As part of Google Summer of Code 2021, we've been working to shorten and simplify the launch of GRASS in Jupyter and imporve the map displays. You can find out more about the project and follow the progress on the [GRASS wiki page](https://trac.osgeo.org/grass/wiki/GSoC/2021/JupyterAndGRASS).\n",
+    "\n",
+    "In addition to simplifying the launch of *GRASS GIS* with `init()`, `grass.jupyter` has two main dislay classes, `GrassRenderer` and `InteractiveMap`. Using the *GRASS* rendering engine in the background,`GrassRenderer` creates maps as PNG images. `InteractiveMap` displays *GRASS GIS* rasters and vectors with [*folium*](http://python-visualization.github.io/folium/), a [*leaflet*](https://leafletjs.com/) library for *Python*.\n",
+    "\n",
+    "This interactive notebook is available online thanks to the [https://mybinder.org](Binder) service. To run the select part (called a *cell*), hit `Shift + Enter`."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Start GRASS GIS"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "import subprocess\n",
+    "import sys\n",
+    "\n",
+    "# Add GRASS to path\n",
+    "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
+    "os.environ[\"GISBASE\"] = gisbase\n",
+    "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))\n",
+    "\n",
+    "# Import GRASS packages\n",
+    "import grass.script as gs\n",
+    "import grass.jupyter as gj\n",
+    "\n",
+    "# Start GRASS Session\n",
+    "gj.init(\"../../data/grassdata\", \"nc_basic_spm_grass7\", \"user1\")\n",
+    "\n",
+    "# Set computational region to the elevation raster.\n",
+    "gs.run_command(\"g.region\", raster=\"elevation\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## GRASS Renderer\n",
+    "\n",
+    "The `GrassRenderer` class creates and displays GRASS maps as PNG images. There are two ways to add elements to the display. First, the name of the *GRASS* display module can be called as an attribute by replacing the \".\" with \"\\_\" in the module name. For example:\n",
+    "````\n",
+    "m = GrassRenderer()\n",
+    "m.d_rast(map=\"elevation\")\n",
+    "````\n",
+    "\n",
+    "Alternatively, *GRASS* display modules can be called with the `run()` method:\n",
+    "````\n",
+    "m = GrassRenderer()\n",
+    "m.run(\"d.rast\", map=\"elevation\")\n",
+    "````\n",
+    "\n",
+    "To display the image, call `show()`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Create GrassRenderer instance\n",
+    "img = gj.GrassRenderer()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Add a raster, vector and legend to the map\n",
+    "img.d_rast(map=\"elevation\")\n",
+    "img.d_vect(map=\"streams\")\n",
+    "img.d_legend(raster=\"elevation\", at=(55, 95, 80, 84), flags=\"b\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Display map\n",
+    "img.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We can also have multiple instances of `GrassRenderer`. Here, we create another map then go back and modify the first map."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Make a second instance.\n",
+    "# Just for variety, we'll make this one a different size\n",
+    "img2 = gj.GrassRenderer(height=200, width=220)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Add a some layers\n",
+    "# We can also add layers with the run() methods\n",
+    "img2.run(\"d.rast\", map=\"elevation_shade\")\n",
+    "img2.run(\"d.vect\", map=\"roadsmajor\")\n",
+    "\n",
+    "# Display second map\n",
+    "img2.show()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Then, we return to the first instance and continue to modify and display it\n",
+    "# Notice that layers a drawn in the order they are added\n",
+    "img.run(\"d.vect\", map = \"zipcodes\", color=\"red\", fill_color=\"none\")\n",
+    "img.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Interactive Map Display\n",
+    "\n",
+    "The `InteractiveMap` class displays *GRASS GIS* rasters and vectors with [*folium*](http://python-visualization.github.io/folium/), a [*leaflet*](https://leafletjs.com/) library for *Python*."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Create Interactive Map\n",
+    "fig = gj.InteractiveMap(width = 600)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Add raster, vector and layer control to map\n",
+    "fig.add_raster(\"elevation\")\n",
+    "fig.add_vector(\"roadsmajor\")\n",
+    "fig.add_layer_control(position = \"bottomright\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Display map\n",
+    "fig.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Save InteractiveMap as HTML\n",
+    "\n",
+    "To share or embed the map in a website, we can export it has an HTML file."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "fig.save(filename=\"test_map.html\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}

+ 382 - 0
doc/notebooks/hydrology.ipynb

@@ -0,0 +1,382 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Hydrology with GRASS GIS\n",
+    "\n",
+    "This is a short introduction to common hydrologic workflows in *GRASS GIS* in *Jupyter Notebook*. In addition to common *Python* packages, it demonstrates the usage of `grass.script`, the *Python* API for GRASS GIS, and `grass.jupyter`, a *Jupyter Notebook* specific package that helps with the launch of *GRASS GIS* and with displaying maps. \n",
+    "\n",
+    "This interactive notebook is available online thanks to the [https://mybinder.org](Binder) service. To run the select part (called a *cell*), hit `Shift + Enter`.\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Starting GRASS in Jupyter Notebooks"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Import Python standard library and IPython packages we need.\n",
+    "import os\n",
+    "import subprocess\n",
+    "import sys\n",
+    "import csv\n",
+    "import numpy as np\n",
+    "import matplotlib.pyplot as plt\n",
+    "from collections import defaultdict\n",
+    "\n",
+    "# Ask GRASS GIS where its Python packages are.\n",
+    "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
+    "os.environ[\"GISBASE\"] = gisbase\n",
+    "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))\n",
+    "\n",
+    "# Import the GRASS GIS packages we need.\n",
+    "import grass.script as gs\n",
+    "import grass.jupyter as gj\n",
+    "\n",
+    "# Start GRASS Session\n",
+    "gj.init(\"../../data/grassdata\", \"nc_basic_spm_grass7\", \"user1\")\n",
+    "\n",
+    "# Set computational region to elevation raster\n",
+    "gs.run_command(\"g.region\", raster=\"elevation\", flags=\"pg\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "First, let's view the elevation raster to get an overview of the area"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Start a GrassRenderer map\n",
+    "# GrassRenderer makes non-interactive maps using a PNG image\n",
+    "img = gj.GrassRenderer()\n",
+    "\n",
+    "# Add a raster, vector and legend to the map\n",
+    "img.d_rast(map=\"elevation\")\n",
+    "img.d_legend(raster=\"elevation\", at=(65, 90, 85, 88), fontsize=12, flags=\"b\", title=\"DTM\")\n",
+    "\n",
+    "# Display map\n",
+    "img.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Depression Filling\n",
+    "\n",
+    "Depression filling is often necessary for certain flow routing algorithms. In this section, we'll find out how extensive the depressions are in our DEM using `r.fill.dir`. Note that r.watershed doesn't need any depression filling thanks to its underlying algorithm which uses least cost path to get over depressions."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.run_command(\"r.fill.dir\", input=\"elevation\", output=\"elev_fill1\", direction=\"dir1\", areas=\"area1\")\n",
+    "gs.run_command(\"r.fill.dir\", input=\"elev_fill1\", output=\"elev_fill2\", direction=\"dir2\", areas=\"area2\")\n",
+    "gs.run_command(\"r.fill.dir\", input=\"elev_fill2\", output=\"elev_fill3\", direction=\"dir3\", areas=\"area3\")\n",
+    "gs.mapcalc(\"depr_bin = if((elevation-elev_fill3) < 0., 1, null())\")\n",
+    "gs.run_command(\"r.colors\", map=\"depr_bin\", color=\"blues\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "# Display the depressions with InteractiveMap to see how they compare to existing waterbodies\n",
+    "depr_map = gj.InteractiveMap()\n",
+    "depr_map.add_raster(\"depr_bin\")\n",
+    "depr_map.add_layer_control()\n",
+    "depr_map.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Computing Watersheds, Drainage Direction, Flow Accumulation, and Streams\n",
+    "\n",
+    "From the elevation raster, we compute the watersheds, drainage direction and flow accumulation and display the results. Since `r.watershed` uses a least cost algorithm, we don't need to use the depression-filled raster; instead, we'll use the original elevation raster.\n",
+    "\n",
+    "It may take a minute for this cell to run."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.run_command(\"r.watershed\", \n",
+    "               elevation=\"elevation@PERMANENT\",\n",
+    "               drainage=\"drainage\",    # Drainage Direction\n",
+    "               accumulation=\"flowacc\", # Flow Accumulation\n",
+    "               basin=\"watersheds\",\n",
+    "               stream=\"streams\",\n",
+    "               threshold=80000)\n",
+    "\n",
+    "# Convert streams raster to vector\n",
+    "gs.run_command(\"r.to.vect\", input=\"streams\", output=\"streams\", type=\"line\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Finally, to view and compare the ouputs of `r.watersheds`, we'll use `grass.jupyter`'s `InteractiveMap` class which allows us to toggle between layers and zoom."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": []
+   },
+   "outputs": [],
+   "source": [
+    "fig = gj.InteractiveMap(height=400, width=600)\n",
+    "\n",
+    "# We can modify with color table for rasters with `r.colors`.\n",
+    "# Note that if the raster is located in a different mapset (for example,\n",
+    "# elevation is in PERMANENT, not user1), the `r.colors` will not change \n",
+    "# the color in InteractiveMap.\n",
+    "gs.run_command(\"r.colors\", map=\"drainage\", color=\"aspect\")\n",
+    "\n",
+    "# Add elements to map\n",
+    "# We set opacity to 1.0 (default is 0.8) so layers won't interfere with eachother.\n",
+    "fig.add_raster(\"elevation\")\n",
+    "fig.add_raster(\"drainage\", opacity=1.0)\n",
+    "fig.add_raster(\"flowacc\", opacity=1.0)\n",
+    "fig.add_raster(\"watersheds\", opacity=1.0)\n",
+    "fig.add_vector(\"streams\")\n",
+    "\n",
+    "fig.add_layer_control()\n",
+    "\n",
+    "fig.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Watershed Area\n",
+    "\n",
+    "With our watersheds, we can compute some zonal statistics. In this section, we use the `count` method in `r.stats.zonal` to make a map of watershed area."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Count cells in each watershed\n",
+    "gs.run_command(\"r.stats.zonal\", base=\"watersheds\", cover=\"elevation\", method=\"count\", output=\"watersheds_count\")\n",
+    "\n",
+    "# Get projection resolution\n",
+    "proj=gs.parse_command(\"g.region\", flags=\"m\")\n",
+    "\n",
+    "# Multiply N-S resollution by E-W resolution to get cell area\n",
+    "cell_area = float(proj[\"nsres\"])*float(proj[\"ewres\"])\n",
+    "\n",
+    "# Calculate watersheds areas and convert from m2 to km2\n",
+    "gs.mapcalc(\"'watershed_area' = float('watersheds_count'*{})/1000000\".format(cell_area))"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Create choropleth map of watershed area."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Display a map of watershed areas. We'll use GrassRenderer here\n",
+    "gs.run_command(\"r.colors\", map=\"watershed_area\", color=\"plasma\")\n",
+    "               \n",
+    "watershed_map = gj.GrassRenderer()\n",
+    "watershed_map.d_rast(map=\"watershed_area\")\n",
+    "watershed_map.d_legend(raster=\"watershed_area\",\n",
+    "                       bgcolor=\"none\",\n",
+    "                       color=\"black\",\n",
+    "                       border_color=\"none\",\n",
+    "                       at=(3, 40, 84, 88),\n",
+    "                       lines=2,\n",
+    "                       fontsize=15,\n",
+    "                       title=\"Area\",\n",
+    "                       units=\" km2\")\n",
+    "watershed_map.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Zonal Statistics: Average Slope by Watershed\n",
+    "\n",
+    "In this section, we compute average slope and standard deviation in each watershed then make a bar plot to compare them. Each watershed is a zone. We use `r.univar` to find compute a table of univariate statistics. An alternative approach would be to use `r.stats.zonal` which returns a raster. \n",
+    "\n",
+    "We start by computing the slope."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Compute Slope\n",
+    "gs.run_command(\"r.slope.aspect\", elevation=\"elevation\", slope=\"slope\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Display slope map\n",
+    "slope_map = gj.GrassRenderer()\n",
+    "slope_map.d_rast(map=\"slope\")\n",
+    "slope_map.d_legend(raster=\"slope\", at=(65, 90, 85, 90), fontsize=15, flags=\"b\", title=\"Slope\", units=\"°\")\n",
+    "slope_map.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now, we use `r.univar` to calculate the average slope in each watershed and return a csv."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "separator = \"|\"\n",
+    "\n",
+    "columns = defaultdict(list) # each value in each column is appended to a list\n",
+    "\n",
+    "text = gs.read_command(\"r.univar\", map=\"elevation\", zones=\"watersheds\", separator=separator, flags=\"t\")\n",
+    "reader = csv.DictReader(text.splitlines(), delimiter=separator)\n",
+    "for row in reader: # read a row as {column1: value1, column2: value2,...}\n",
+    "    for (k,v) in row.items(): # go over each column name and value \n",
+    "        columns[k].append(v) # append the value into the appropriate list\n",
+    "                             # based on column name k\n",
+    "\n",
+    "watersheds = columns[\"zone\"]\n",
+    "means = np.array(columns[\"mean\"], dtype=np.float32)\n",
+    "stddevs = np.array(columns[\"stddev\"], dtype=np.float32)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Make a bar plot of average slope by watershed\n",
+    "bar_positions = np.arange(len(watersheds))\n",
+    "plt.style.use(\"ggplot\")\n",
+    "fig, ax = plt.subplots()\n",
+    "ax.set_title(\"Average Slope\", fontsize=16)\n",
+    "ax.set_xlabel(\"Watershed\")\n",
+    "ax.set_ylabel(\"Slope [degrees]\")\n",
+    "ax.bar(bar_positions, means)\n",
+    "ax.set_xticks(bar_positions)\n",
+    "ax.set_xticklabels(watersheds)\n",
+    "plt.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Converting to Vectors\n",
+    "\n",
+    "Convert watersheds from raster to vector to make a nice map. Label the watersheds so we can compare to the bar chart above."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Convert to vector\n",
+    "gs.run_command(\"r.to.vect\", flags=\"s\", input=\"watersheds\", output=\"watersheds_vector\", type=\"area\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Display\n",
+    "watershed_map2 = gj.GrassRenderer()\n",
+    "watershed_map2.d_rast(map=\"elevation\")\n",
+    "watershed_map2.d_vect(map=\"watersheds_vector\",\n",
+    "                      fill_color=\"none\",\n",
+    "                      width=1.5,\n",
+    "                      color=\"black\",\n",
+    "                      attribute_column=\"value\",\n",
+    "                      label_bgcolor=\"black\",\n",
+    "                      label_color=\"white\",\n",
+    "                      label_size=10)\n",
+    "watershed_map2.show()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}

+ 0 - 193
doc/notebooks/jupyter_integration.ipynb

@@ -1,193 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# Improved Integration of GRASS and Jupyter\n",
-    "\n",
-    "As part of Google Summer of Code 2021, we've been working to shorten and simplify the launch of GRASS in Jupyter and imporve the map displays. You can find out more about the project and follow the progress on the [GRASS wiki page](https://trac.osgeo.org/grass/wiki/GSoC/2021/JupyterAndGRASS).\n",
-    "\n",
-    "This notebook is designed to run in binder and demonstrate the usage of `grass.jupyter`, the new module of Jupyter-specific functions for GRASS."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [],
-   "source": [
-    "import os\n",
-    "import subprocess\n",
-    "import sys"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
-    "os.environ[\"GISBASE\"] = gisbase\n",
-    "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import grass.script as gs\n",
-    "import grass.jupyter as gj"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Start GRASS Session\n",
-    "gj.init(\"../../data/grassdata\", \"nc_basic_spm_grass7\", \"user1\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Set computational region to the study area.\n",
-    "gs.run_command(\"g.region\", raster=\"elevation\")"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Non-interactive Map Display"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Demonstration of GrassRenderer for non-interactive map display\n",
-    "r = gj.GrassRenderer(height=540)\n",
-    "\n",
-    "# Add a raster and vector to the map\n",
-    "r.run(\"d.rast\", map=\"elevation\")\n",
-    "r.run(\"d.vect\", map=\"streams\")\n",
-    "\n",
-    "# We can also call 'd.*' display modules with a shortcut\n",
-    "# Shortcut methods must be in the form '.d_{name_of_module}'\n",
-    "# For example, let's add a legend using a shortcut\n",
-    "r.d_legend(raster=\"elevation\", at=(55, 95, 2, 6), flags=\"b\") # shortcut for calling \"d.legend\"\n",
-    "\n",
-    "# Display map\n",
-    "r.show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "#### Multiple instances of GrassRenderer\n",
-    "\n",
-    "We can have multiple instances of GrassRenderer."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# First, we'll make a second instance. Notice we need a different filename.\n",
-    "\n",
-    "r2 = gj.GrassRenderer(height=200, width=220, filename =\"roads_maps.png\")\n",
-    "\n",
-    "r2.run(\"d.rast\", map=\"elevation_shade\")\n",
-    "r2.run(\"d.vect\", map=\"roadsmajor\")\n",
-    "\n",
-    "r2.show()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Then, we return to the first instance and continue to modify and display it\n",
-    "# Notice that layers a drawn in the order they are added\n",
-    "\n",
-    "r.run(\"d.vect\", map = \"zipcodes\", color=\"red\", fill_color=\"none\")\n",
-    "\n",
-    "r.show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Interactive Map Display"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Create Interactive Map\n",
-    "m = gj.InteractiveMap(width = 600)\n",
-    "\n",
-    "# Add raster, vector and layer control to map\n",
-    "m.add_raster(\"elevation\")\n",
-    "m.add_vector(\"roadsmajor\")\n",
-    "m.add_layer_control(position = \"bottomright\")\n",
-    "\n",
-    "# Display map\n",
-    "m.show()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Let's see what is in the example database so we can continue to experiment\n",
-    "print(gs.read_command(\"g.list\", type=\"all\"))"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.8.10"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}

+ 230 - 0
doc/notebooks/solar_potential.ipynb

@@ -0,0 +1,230 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "33082a70-1d2d-4bfa-bc45-981115829941",
+   "metadata": {},
+   "source": [
+    "# Solar Energy Potential Analysis\n",
+    "\n",
+    "This is an introduction to solar radiation modeling in *GRASS GIS* in *Jupyter Notebook*. In addition to common *Python* packages, it demonstrates the usage of `grass.script`, the *Python* API for GRASS GIS, and `grass.jupyter`, a *Jupyter Notebook* specific package that helps with the launch of *GRASS GIS* and with displaying maps. \n",
+    "\n",
+    "This interactive notebook is available online thanks to the [https://mybinder.org](Binder) service. To run the select part (called a *cell*), hit `Shift + Enter`."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2b47cfa6-312b-4db9-873d-7e2333589411",
+   "metadata": {},
+   "source": [
+    "## Starting GRASS in Jupyter Notebooks"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "66e65bb3-6401-448f-bc1e-475ac5fcf1ad",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Import Python standard library and IPython packages we need.\n",
+    "import os\n",
+    "import subprocess\n",
+    "import sys\n",
+    "\n",
+    "# Ask GRASS GIS where its Python packages are.\n",
+    "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
+    "os.environ[\"GISBASE\"] = gisbase\n",
+    "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))\n",
+    "\n",
+    "# Import the GRASS GIS packages we need.\n",
+    "import grass.script as gs\n",
+    "import grass.jupyter as gj\n",
+    "\n",
+    "# Start GRASS Session\n",
+    "gj.init(\"../../data/grassdata\", \"nc_basic_spm_grass7\", \"user1\")\n",
+    "\n",
+    "# Set computational region to elevation raster\n",
+    "gs.run_command(\"g.region\", raster=\"elevation@PERMANENT\", flags=\"pg\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "70a1e6ee-39a0-4936-b656-95976c3cce44",
+   "metadata": {
+    "tags": []
+   },
+   "source": [
+    "## Solar radiation analysis"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8a7d0768-80ca-4190-ba8c-d19b4719e7d2",
+   "metadata": {},
+   "source": [
+    "Prepare input maps (slope and aspect):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bca26a9a-126b-41d0-a8db-c58e4ac9c58d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.run_command(\"r.slope.aspect\", elevation=\"elevation\", aspect=\"aspect\", slope=\"slope\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f3567f03-624b-4a17-8463-6715f86e6b75",
+   "metadata": {},
+   "source": [
+    "### Incidence angles and cast shadows\n",
+    "\n",
+    "Compute the sun position on Dec. 22 at 4:15pm, EST (no map output expected):"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0d01b59b-2c2e-440b-886d-bb484451a8ba",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.run_command(\"r.sunmask\", elevation=\"elevation\", year=\"2001\", month=\"12\", day=\"22\", hour=\"16\", minute=\"15\", sec=\"0\", timezone=\"-5\", flags=\"s\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "311fe6d4-a2d4-4295-9e86-798f0debe28d",
+   "metadata": {},
+   "source": [
+    "Calculate incidence angles including cast shadows."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "116f5f2c-48e0-483b-b49d-3595196d8714",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.run_command(\"r.sun\", elevation=\"elevation\", aspect=\"aspect\", slope=\"slope\", incidout=\"incident\", day=\"356\", time=\"16.25\")\n",
+    "gs.parse_command(\"r.info\", map=\"incident\", flags=\"g\")\n",
+    "gs.run_command(\"r.colors\", map=\"incident\", co=\"bcyr\", flags=\"e\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "8374778c-339f-410a-a2c7-099cabef20f3",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Display incident angles\n",
+    "solar_map = gj.GrassRenderer()\n",
+    "solar_map.d_rast(map=\"incident\")\n",
+    "solar_map.d_legend(raster=\"incident\", fontsize=12, at=\"25,75,1,5\")\n",
+    "solar_map.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "b0f211b3-e5d7-4051-9a80-0f5ce0cf9665",
+   "metadata": {},
+   "source": [
+    "Extract the cast shadow area for 4:15pm."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d64db7b3-c22a-4a26-a32b-fccc7eb0858d",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.mapcalc(\"shadow = if(isnull(incident), 1, null())\")\n",
+    "gs.run_command(\"r.colors\", map=\"shadow\", color=\"grey\")\n",
+    "gs.run_command(\"r.colors\", map=\"elevation\", color=\"elevation\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2ccf0d4e-633f-463c-bb12-6fe151a03fb1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Display 4:15pm Shadows\n",
+    "shadow_map = gj.GrassRenderer()\n",
+    "shadow_map.d_shade(shade=\"shadow\", color=\"elevation\")\n",
+    "shadow_map.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "30758fb8-7658-41d3-ad25-199bf8e8e4e7",
+   "metadata": {},
+   "source": [
+    "### Solar radiation\n",
+    "Compute global (beam+diffuse+refl) radiation for entire day during summer and winter solstice. This may take a minute to run.\n",
+    "\n",
+    "Display the radiation maps."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "753dcb19-a2f1-4329-ba39-912e709ff85e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.run_command(\"r.sun\", elevation=\"elevation\", aspect=\"aspect\", slope=\"slope\", day=\"356\", glob_rad=\"winter\", insol_time=\"its356\")\n",
+    "gs.run_command(\"r.colors\", map=\"winter\", co=\"gyr\", flags=\"e\")\n",
+    "\n",
+    "gs.run_command(\"r.sun\", elevation=\"elevation\", aspect=\"aspect\", slope=\"slope\", day=\"172\", glob_rad=\"summer\", insol_time=\"its172\")\n",
+    "gs.run_command(\"r.colors\", map=\"summer\", co=\"gyr\", flags=\"e\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0a27993b-126a-44c8-8102-dd85ee9aee96",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "rad_map = gj.InteractiveMap()\n",
+    "\n",
+    "# Set opacity to 1.0 (default is 0.8) so that we can see colors clearly\n",
+    "rad_map.add_raster(\"winter\", opacity=1.0)\n",
+    "rad_map.add_raster(\"summer\", opacity=1.0)\n",
+    "rad_map.add_layer_control()\n",
+    "\n",
+    "rad_map.show()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}

+ 116 - 0
doc/notebooks/viewshed_analysis.ipynb

@@ -0,0 +1,116 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Viewshed Analysis"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This notebook is a basic introduction to viewshed analysis in *GRASS GIS* in *Jupyter Notebook*. In addition to common *Python* packages, it demonstrates the usage of `grass.script`, the *Python* API for GRASS GIS, and `grass.jupyter`, a *Jupyter Notebook* specific package that helps with the launch of *GRASS GIS* and with displaying maps. \n",
+    "\n",
+    "This interactive notebook is available online thanks to the [https://mybinder.org](Binder) service. To run the select part (called a *cell*), hit `Shift + Enter`."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Starting GRASS in Jupyter Notebooks"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Import Python standard library and IPython packages we need.\n",
+    "import os\n",
+    "import subprocess\n",
+    "import sys\n",
+    "\n",
+    "# Ask GRASS GIS where its Python packages are.\n",
+    "gisbase = subprocess.check_output([\"grass\", \"--config\", \"path\"], text=True).strip()\n",
+    "os.environ[\"GISBASE\"] = gisbase\n",
+    "sys.path.append(os.path.join(gisbase, \"etc\", \"python\"))\n",
+    "\n",
+    "# Import the GRASS GIS packages we need.\n",
+    "import grass.script as gs\n",
+    "import grass.jupyter as gj\n",
+    "\n",
+    "# Start GRASS Session\n",
+    "gj.init(\"../../data/grassdata\", \"nc_basic_spm_grass7\", \"user1\")\n",
+    "\n",
+    "# Set computational region to elevation raster\n",
+    "gs.run_command(\"g.region\", raster=\"elevation@PERMANENT\", flags=\"pg\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Viewshed analysis\n",
+    "Compute viewshed from a new 32 story tower located in downtown Raleigh, NC."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "gs.parse_command(\"g.region\", raster=\"elevation\", flags=\"apg\")\n",
+    "gs.write_command(\"v.in.ascii\", input=\"-\", stdin=\"%s|%s\" % (642212, 224767), output=\"viewpoints\")\n",
+    "gs.run_command(\"r.viewshed\", input=\"elevation\", output=\"tower_los\", coordinates=\"642212,224767\", observer_elevation=\"165\", max_distance=\"10000\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Display result with basemap:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "vs_map = gj.InteractiveMap()\n",
+    "\n",
+    "vs_map.add_raster(\"tower_los\", opacity=0.7)\n",
+    "vs_map.add_vector(\"viewpoints\")\n",
+    "vs_map.add_layer_control()\n",
+    "\n",
+    "vs_map.show()"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.10"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}

+ 6 - 0
python/grass/jupyter/display.py

@@ -45,6 +45,7 @@ class GrassRenderer:
         width=600,
         width=600,
         filename=None,
         filename=None,
         env=None,
         env=None,
+        font="sans",
         text_size=12,
         text_size=12,
         renderer="cairo",
         renderer="cairo",
     ):
     ):
@@ -55,6 +56,10 @@ class GrassRenderer:
         :param int width: width of map in pixels
         :param int width: width of map in pixels
         :param str filename: filename or path to save a PNG of map
         :param str filename: filename or path to save a PNG of map
         :param str env: environment
         :param str env: environment
+        :param str font: font to use in rendering; either the name of a font from
+                        $GISBASE/etc/fontcap (or alternative fontcap file specified
+                        by GRASS_FONT_CAP), or alternatively the full path to a FreeType
+                        font file
         :param int text_size: default text size, overwritten by most display modules
         :param int text_size: default text size, overwritten by most display modules
         :param renderer: GRASS renderer driver (options: cairo, png, ps, html)
         :param renderer: GRASS renderer driver (options: cairo, png, ps, html)
         """
         """
@@ -67,6 +72,7 @@ class GrassRenderer:
         # Environment Settings
         # Environment Settings
         self._env["GRASS_RENDER_WIDTH"] = str(width)
         self._env["GRASS_RENDER_WIDTH"] = str(width)
         self._env["GRASS_RENDER_HEIGHT"] = str(height)
         self._env["GRASS_RENDER_HEIGHT"] = str(height)
+        self._env["GRASS_FONT"] = font
         self._env["GRASS_RENDER_TEXT_SIZE"] = str(text_size)
         self._env["GRASS_RENDER_TEXT_SIZE"] = str(text_size)
         self._env["GRASS_RENDER_IMMEDIATE"] = renderer
         self._env["GRASS_RENDER_IMMEDIATE"] = renderer
         self._env["GRASS_RENDER_FILE_READ"] = "TRUE"
         self._env["GRASS_RENDER_FILE_READ"] = "TRUE"