ソースを参照

Jupyter: Add non-interactive display (#1668)

* 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.
Caitlin H 3 年 前
コミット
86acc1cb08

+ 79 - 15
doc/notebooks/jupyter_integration.ipynb

@@ -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."
    ]
@@ -21,8 +21,7 @@
    "source": [
     "import os\n",
     "import subprocess\n",
-    "import sys\n",
-    "from IPython.display import Image"
+    "import sys"
    ]
   },
   {
@@ -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"
    ]
   },
   {
@@ -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": {

+ 1 - 1
python/grass/jupyter/Makefile

@@ -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__)

+ 1 - 0
python/grass/jupyter/__init__.py

@@ -1 +1,2 @@
 from .setup import *
+from .display import *

+ 58 - 0
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)

+ 0 - 25
python/grass/jupyter/setup.py

@@ -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"