display.py 5.9 KB

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