display.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. # MODULE: grass.jupyter.display
  2. #
  3. # AUTHOR(S): Caitlin Haedrich <caitlin DOT haedrich AT gmail>
  4. #
  5. # PURPOSE: This module contains functions for non-interactive display
  6. # in Jupyter Notebooks
  7. #
  8. # COPYRIGHT: (C) 2021 Caitlin Haedrich, and by the GRASS Development Team
  9. #
  10. # This program is free software under the GNU General Public
  11. # License (>=v2). Read the file COPYING that comes with GRASS
  12. # for details.
  13. """2D rendering and display functionality"""
  14. import os
  15. import shutil
  16. import tempfile
  17. import weakref
  18. import grass.script as gs
  19. from .region import RegionManagerFor2D
  20. class GrassRenderer:
  21. """GrassRenderer creates and displays GRASS maps in
  22. Jupyter Notebooks.
  23. Elements are added to the display by calling GRASS display modules.
  24. Basic usage::
  25. >>> m = GrassRenderer()
  26. >>> m.run("d.rast", map="elevation")
  27. >>> m.run("d.legend", raster="elevation")
  28. >>> m.show()
  29. GRASS display modules can also be called by using the name of module
  30. as a class method and replacing "." with "_" in the name.
  31. Shortcut usage::
  32. >>> m = GrassRenderer()
  33. >>> m.d_rast(map="elevation")
  34. >>> m.d_legend(raster="elevation")
  35. >>> m.show()
  36. """
  37. def __init__(
  38. self,
  39. height=400,
  40. width=600,
  41. filename=None,
  42. env=None,
  43. font="sans",
  44. text_size=12,
  45. renderer="cairo",
  46. use_region=False,
  47. saved_region=None,
  48. ):
  49. """Creates an instance of the GrassRenderer class.
  50. :param int height: height of map in pixels
  51. :param int width: width of map in pixels
  52. :param str filename: filename or path to save a PNG of map
  53. :param str env: environment
  54. :param str font: font to use in rendering; either the name of a font from
  55. $GISBASE/etc/fontcap (or alternative fontcap file specified
  56. by GRASS_FONT_CAP), or alternatively the full path to a FreeType
  57. font file
  58. :param int text_size: default text size, overwritten by most display modules
  59. :param renderer: GRASS renderer driver (options: cairo, png, ps, html)
  60. :param use_region: if True, use either current or provided saved region,
  61. else derive region from rendered layers
  62. :param saved_region: if name of saved_region is provided,
  63. this region is then used for rendering
  64. """
  65. # Copy Environment
  66. if env:
  67. self._env = env.copy()
  68. else:
  69. self._env = os.environ.copy()
  70. # Environment Settings
  71. self._env["GRASS_RENDER_WIDTH"] = str(width)
  72. self._env["GRASS_RENDER_HEIGHT"] = str(height)
  73. self._env["GRASS_FONT"] = font
  74. self._env["GRASS_RENDER_TEXT_SIZE"] = str(text_size)
  75. self._env["GRASS_RENDER_IMMEDIATE"] = renderer
  76. self._env["GRASS_RENDER_FILE_READ"] = "TRUE"
  77. self._env["GRASS_RENDER_TRANSPARENT"] = "TRUE"
  78. # Create PNG file for map
  79. # If not user-supplied, we will write it to a map.png in a
  80. # temporary directory that we can delete later. We need
  81. # this temporary directory for the legend anyways so we'll
  82. # make it now
  83. # Resource managed by weakref.finalize.
  84. self._tmpdir = (
  85. tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
  86. )
  87. def cleanup(tmpdir):
  88. tmpdir.cleanup()
  89. weakref.finalize(self, cleanup, self._tmpdir)
  90. if filename:
  91. self._filename = filename
  92. else:
  93. self._filename = os.path.join(self._tmpdir.name, "map.png")
  94. # Set environment var for file
  95. self._env["GRASS_RENDER_FILE"] = self._filename
  96. # Create Temporary Legend File
  97. self._legend_file = os.path.join(self._tmpdir.name, "legend.txt")
  98. self._env["GRASS_LEGEND_FILE"] = str(self._legend_file)
  99. # rendering region setting
  100. self._region_manager = RegionManagerFor2D(use_region, saved_region, self._env)
  101. @property
  102. def filename(self):
  103. """Filename or full path to the file with the resulting image.
  104. The value can be set during initialization. When the filename was not provided
  105. during initialization, a path to temporary file is returned. In that case, the
  106. file is guaranteed to exist as long as the object exists.
  107. """
  108. return self._filename
  109. @property
  110. def region_manager(self):
  111. """Region manager object"""
  112. return self._region_manager
  113. def run(self, module, **kwargs):
  114. """Run modules from the GRASS display family (modules starting with "d.").
  115. This function passes arguments directly to grass.script.run_command()
  116. so the syntax is the same.
  117. :param str module: name of GRASS module
  118. :param `**kwargs`: named arguments passed to run_command()"""
  119. # Check module is from display library then run
  120. if module[0] == "d":
  121. self._region_manager.set_region_from_command(module, **kwargs)
  122. gs.run_command(module, env=self._env, **kwargs)
  123. else:
  124. raise ValueError("Module must begin with letter 'd'.")
  125. def __getattr__(self, name):
  126. """Parse attribute to GRASS display module. Attribute should be in
  127. the form 'd_module_name'. For example, 'd.rast' is called with 'd_rast'.
  128. """
  129. # Check to make sure format is correct
  130. if not name.startswith("d_"):
  131. raise AttributeError(_("Module must begin with 'd_'"))
  132. # Reformat string
  133. grass_module = name.replace("_", ".")
  134. # Assert module exists
  135. if not shutil.which(grass_module):
  136. raise AttributeError(_("Cannot find GRASS module {}").format(grass_module))
  137. def wrapper(**kwargs):
  138. # Run module
  139. self.run(grass_module, **kwargs)
  140. return wrapper
  141. def show(self):
  142. """Displays a PNG image of map"""
  143. # Lazy import to avoid an import-time dependency on IPython.
  144. from IPython.display import Image # pylint: disable=import-outside-toplevel
  145. return Image(self._filename)