Skip to content

Commit

Permalink
Jupyter: Add non-interactive display (#1668)
Browse files Browse the repository at this point in the history
* Non-interactive display functions with rendering based on d-commands.
* Rendering encapsulated in a class. Each object holds unique settings.
* Examples in notebook are now showing this new API.
* Deleted display_settings function.
* Rendering class init accepts env parameter as all well-behaved functions in grass.script.
* grass.script is used as a backend for running commands.
* Rendering run method provides general way of rendering with any d-command (which is not monitor-focused).
* Errors reported as exceptions, e.g., ValueError.
* Added pathlib Path object supported for filenames.
  • Loading branch information
chaedri committed Jul 2, 2021
1 parent 3d86470 commit 86acc1c
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 41 deletions.
94 changes: 79 additions & 15 deletions doc/notebooks/jupyter_integration.ipynb
Expand Up @@ -6,7 +6,7 @@
"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 abou the project and follow the progress on the [GRASS wiki page](https://trac.osgeo.org/grass/wiki/GSoC/2021/JupyterAndGRASS).\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."
]
Expand All @@ -21,8 +21,7 @@
"source": [
"import os\n",
"import subprocess\n",
"import sys\n",
"from IPython.display import Image"
"import sys"
]
},
{
Expand Down Expand Up @@ -63,10 +62,41 @@
"outputs": [],
"source": [
"# Start GRASS Session\n",
"gj.init(\"../../data/grassdata\", \"nc_basic_spm_grass7\", \"user1\")\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": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# Demonstration of GrassRenderer for non-interactive map display\n",
"m = gj.GrassRenderer(height=540, filename = \"streams_map.png\")\n",
"\n",
"# Set default display settings\n",
"gj.display_settings()"
"m.run(\"d.rast\", map=\"elevation\")\n",
"m.run(\"d.vect\", map=\"streams\")\n",
"\n",
"m.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Let's demonstrate how we can have two instances of GrassRenderer"
]
},
{
Expand All @@ -75,18 +105,52 @@
"metadata": {},
"outputs": [],
"source": [
"# Let's display the DTM of our sample area to ensure all's working\n",
"# First, we'll make a second instance. Notice we need a different filename\n",
"m2 = gj.GrassRenderer(height=200, width = 220, filename = \"roads_maps.png\")\n",
"\n",
"# Set computational region to the study area.\n",
"gs.parse_command(\"g.region\", raster=\"elevation\", flags=\"pg\")\n",
"m2.run(\"d.rast\", map=\"elevation_shade\")\n",
"m2.run(\"d.vect\", map=\"roadsmajor\")\n",
"\n",
"m2.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Then, we return to the first instance and continue to modify/display\n",
"\n",
"m.run(\"d.vect\", map = \"zipcodes\", color=\"red\", fill_color=\"none\")\n",
"\n",
"# Draw elevation (DTM) to get an overview of the area.\n",
"gs.run_command(\"r.colors\", map=\"elevation\", color=\"elevation\")\n",
"gs.run_command(\"d.erase\")\n",
"gs.run_command(\"d.rast\", map=\"elevation\")\n",
"gs.run_command(\"d.legend\", raster=\"elevation\", at=(65, 90, 85, 90), fontsize=15, flags=\"b\", title=\"DTM\")\n",
"Image(filename=\"map.png\")"
"m.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Error Handling"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# If we pass a non-display related module to GrassRenderer, it returns an error\n",
"\n",
"m.run(\"r.watershed\", map=\"elevation\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
2 changes: 1 addition & 1 deletion python/grass/jupyter/Makefile
Expand Up @@ -5,7 +5,7 @@ include $(MODULE_TOPDIR)/include/Make/Python.make

DSTDIR = $(ETC)/python/grass/jupyter

MODULES = setup
MODULES = setup display

PYFILES := $(patsubst %,$(DSTDIR)/%.py,$(MODULES) __init__)
PYCFILES := $(patsubst %,$(DSTDIR)/%.pyc,$(MODULES) __init__)
Expand Down
1 change: 1 addition & 0 deletions python/grass/jupyter/__init__.py
@@ -1 +1,2 @@
from .setup import *
from .display import *
58 changes: 58 additions & 0 deletions python/grass/jupyter/display.py
@@ -0,0 +1,58 @@
# MODULE: grass.jupyter.display
#
# AUTHOR(S): Caitlin Haedrich <caitlin DOT haedrich AT gmail>
#
# PURPOSE: This module contains functions for non-interactive display
# in Jupyter Notebooks
#
# COPYRIGHT: (C) 2021 Caitlin Haedrich, and by the GRASS Development Team
#
# This program is free software under the GNU General Public
# License (>=v2). Read the file COPYING that comes with GRASS
# for details.

import os
from pathlib import Path
from IPython.display import Image
import grass.script as gs


class GrassRenderer:
"""The grassRenderer class creates and displays GRASS maps in
Jupyter Notebooks."""

def __init__(
self, env=None, width=600, height=400, filename="map.png", text_size=12
):
"""Initiates an instance of the GrassRenderer class."""

if env:
self._env = env.copy()
else:
self._env = os.environ.copy()

self._env["GRASS_RENDER_WIDTH"] = str(width)
self._env["GRASS_RENDER_HEIGHT"] = str(height)
self._env["GRASS_TEXT_SIZE"] = str(text_size)
self._env["GRASS_RENDER_IMMEDIATE"] = "cairo"
self._env["GRASS_RENDER_FILE"] = str(filename)
self._env["GRASS_RENDER_FILE_READ"] = "TRUE"

self._legend_file = Path(filename).with_suffix(".grass_vector_legend")
self._env["GRASS_LEGEND_FILE"] = str(self._legend_file)

self._filename = filename

self.run("d.erase")

def run(self, module, **kwargs):
"""Run modules from "d." GRASS library"""
# Check module is from display library then run
if module[0] == "d":
gs.run_command(module, env=self._env, **kwargs)
else:
raise ValueError("Module must begin with letter 'd'.")

def show(self):
"""Displays a PNG image of the map (non-interactive)"""
return Image(self._filename)
25 changes: 0 additions & 25 deletions python/grass/jupyter/setup.py
Expand Up @@ -47,28 +47,3 @@ def init(path, location, mapset):
gsetup.init(os.environ["GISBASE"], path, location, mapset)
# Set GRASS env. variables
_set_notebook_defaults()


def display_settings(font="sans", driver="cairo"):
"""
This function sets the display settings for a GRASS session
in Jupyter Notebooks.
Example Usage: display_settings(font="sans", driver="cairo")
Inputs:
font - specifies the font as 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.
driver - tell teh display library which driver to use
Possible values: "cairo", "png", "ps", "html"
"""
# Set display font
os.environ["GRASS_FONT"] = font

# Set display modeules to render to a file (named map.png by
# default).
os.environ["GRASS_RENDER_IMMEDIATE"] = driver
os.environ["GRASS_RENDER_FILE_READ"] = "TRUE"

0 comments on commit 86acc1c

Please sign in to comment.