# MODULE: grass.jupyter.map
#
# AUTHOR(S): Caitlin Haedrich <caitlin DOT haedrich AT gmail>
#
# PURPOSE: This module contains functions for non-interactive display
# in Jupyter Notebooks
#
# COPYRIGHT: (C) 2021-2022 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.
"""2D rendering and display functionality"""
import os
import shutil
import tempfile
import weakref
from pathlib import Path
from grass.tools import Tools
from grass.tools.support import ToolFunctionResolver
from .region import RegionManagerFor2D
[docs]
class Map:
"""Map creates and displays GRASS maps in
Jupyter Notebooks.
Elements are added to the display by calling GRASS display modules.
:Basic usage:
.. code-block:: pycon
>>> m = Map()
>>> m.run("d.rast", map="elevation")
>>> m.run("d.legend", raster="elevation")
>>> m.show()
GRASS display modules can also be called by using the name of module
as a class method and replacing "." with "_" in the name.
:Shortcut usage:
.. code-block:: pycon
>>> m = Map()
>>> m.d_rast(map="elevation")
>>> m.d_legend(raster="elevation")
>>> m.show()
"""
def __init__(
self,
width=None,
height=None,
filename=None,
env=None,
session=None,
font="sans",
text_size=12,
renderer="cairo",
use_region=False,
saved_region=None,
read_file=False,
):
"""Creates an instance of the Map class.
:param int height: height 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 env: runtime environment to use for execution (defaults to global)
:param str session: session with environment (used if it has an env attribute)
: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 renderer: GRASS renderer driver (options: cairo, png, ps, html)
:param use_region: if True, use either current or provided saved region,
else derive region from rendered layers
:param saved_region: if name of saved_region is provided,
this region is then used for rendering
:param bool read_file: if False (default), erase filename before re-writing to
clear contents. If True, read file without clearing contents
first.
"""
# Copy Environment
if env:
self._env = env.copy()
elif session and hasattr(session, "env"):
self._env = session.env.copy()
else:
self._env = os.environ.copy()
# Environment Settings
self._env["GRASS_RENDER_WIDTH"] = str(width) if width else "600"
self._env["GRASS_RENDER_HEIGHT"] = str(height) if height else "400"
self._env["GRASS_FONT"] = font
self._env["GRASS_RENDER_TEXT_SIZE"] = str(text_size)
self._env["GRASS_RENDER_IMMEDIATE"] = renderer
self._env["GRASS_RENDER_FILE_READ"] = "TRUE"
self._env["GRASS_RENDER_TRANSPARENT"] = "TRUE"
# Create PNG file for map
# If not user-supplied, we will write it to a map.png in a
# temporary directory that we can delete later. We need
# this temporary directory for the legend anyways so we'll
# make it now
# Resource managed by weakref.finalize.
self._tmpdir = (
tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
)
def cleanup(tmpdir):
tmpdir.cleanup()
weakref.finalize(self, cleanup, self._tmpdir)
if filename:
self._filename = filename
if not read_file and Path(self._filename).exists():
os.remove(self._filename)
else:
self._filename = os.path.join(self._tmpdir.name, "map.png")
# Set environment var for file
self._env["GRASS_RENDER_FILE"] = self._filename
# Create Temporary Legend File
self._legend_file = os.path.join(self._tmpdir.name, "legend.txt")
self._env["GRASS_LEGEND_FILE"] = str(self._legend_file)
# rendering region setting
self._region_manager = RegionManagerFor2D(
use_region=use_region,
saved_region=saved_region,
width=width,
height=height,
env=self._env,
)
self._name_resolver = None
@property
def filename(self):
"""Filename or full path to the file with the resulting image.
The value can be set during initialization. When the filename was not provided
during initialization, a path to temporary file is returned. In that case, the
file is guaranteed to exist as long as the object exists.
"""
return self._filename
@property
def region_manager(self):
"""Region manager object"""
return self._region_manager
[docs]
def run(self, tool_name_: str, /, **kwargs):
"""Run tools from the GRASS display family (tools starting with "d").
This function passes arguments directly to grass.tools.run(),
so the syntax is the same.
:param tool_name_: name of a GRASS tool
:param `**kwargs`: parameters passed to the tool
"""
# Check module is from display library then run
if tool_name_[0] != "d":
msg = f"Tool must be a display tool starting with 'd', got: {tool_name_}"
raise ValueError(msg)
self._region_manager.set_region_from_command(tool_name_, **kwargs)
self._region_manager.adjust_rendering_size_from_region()
Tools(env=self._env).run(tool_name_, **kwargs)
def __getattr__(self, name):
"""Get a function representing a GRASS display tool.
Attributes should be in the form 'd_tool_name'.
For example, 'd.rast' is called with 'd_rast'.
"""
if not self._name_resolver:
self._name_resolver = ToolFunctionResolver(
run_function=self.run,
env=self._env,
allowed_prefix="d_",
)
return self._name_resolver.get_function(name, exception_type=AttributeError)
def __dir__(self):
"""List available tools and standard attributes."""
if not self._name_resolver:
self._name_resolver = ToolFunctionResolver(
run_function=self.run,
env=self._env,
allowed_prefix="d_",
)
# Collect instance and class attributes
static_attrs = set(dir(type(self))) | set(self.__dict__.keys())
return list(static_attrs) + self._name_resolver.names()
[docs]
def show(self):
"""Displays a PNG image of map"""
# Lazy import to avoid an import-time dependency on IPython.
from IPython.display import Image, display # pylint: disable=import-outside-toplevel
display(Image(self._filename))
[docs]
def save(self, filename):
"""Saves a PNG image of map to the specified *filename*"""
shutil.copy(self._filename, filename)