interact_display.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. #
  2. # AUTHOR(S): Caitlin Haedrich <caitlin DOT haedrich AT gmail>
  3. #
  4. # PURPOSE: This module contains functions for interactive display
  5. # in Jupyter Notebooks.
  6. #
  7. # COPYRIGHT: (C) 2021 Caitlin Haedrich, and by the GRASS Development Team
  8. #
  9. # This program is free software under the GNU General Public
  10. # License (>=v2). Read the file COPYING that comes with GRASS
  11. # for details.
  12. import os
  13. import sys
  14. import tempfile
  15. import weakref
  16. from pathlib import Path
  17. import grass.script as gs
  18. from .display import GrassRenderer
  19. from .utils import (
  20. estimate_resolution,
  21. get_location_proj_string,
  22. get_region,
  23. reproject_region,
  24. setup_location,
  25. )
  26. class InteractiveMap:
  27. """This class creates interative GRASS maps with folium.
  28. Basic Usage:
  29. >>> m = InteractiveMap()
  30. >>> m.add_vector("streams")
  31. >>> m.add_raster("elevation")
  32. >>> m.add_layer_control()
  33. >>> m.show()
  34. """
  35. def __init__(self, width=400, height=400):
  36. """Creates a blank folium map centered on g.region.
  37. :param int height: height in pixels of figure (default 400)
  38. :param int width: width in pixels of figure (default 400)
  39. """
  40. import folium
  41. self._folium = folium
  42. # Store height and width
  43. self.width = width
  44. self.height = height
  45. # Make temporary folder for all our files
  46. self._tmp_dir = tempfile.TemporaryDirectory()
  47. # Remember original environment; all environments used
  48. # in this class are derived from this one
  49. self._src_env = os.environ.copy()
  50. # Set up temporary locations in WGS84 and Pseudo-Mercator
  51. # We need two because folium uses WGS84 for vectors and coordinates
  52. # and Pseudo-Mercator for raster overlays
  53. self.rcfile_psmerc, self._psmerc_env = setup_location(
  54. "psmerc", self._tmp_dir.name, "3857", self._src_env
  55. )
  56. self.rcfile_wgs84, self._wgs84_env = setup_location(
  57. "wgs84", self._tmp_dir.name, "4326", self._src_env
  58. )
  59. # Get Center of temporary GRASS regions
  60. center = gs.parse_command("g.region", flags="cg", env=self._wgs84_env)
  61. center = (float(center["center_northing"]), float(center["center_easting"]))
  62. # Create Folium Map
  63. self.map = self._folium.Map(
  64. width=self.width,
  65. height=self.height,
  66. location=center,
  67. tiles="cartodbpositron",
  68. )
  69. # Set LayerControl default
  70. self.layer_control = False
  71. # Cleanup rcfiles with finalizer
  72. def remove_if_exists(path):
  73. if sys.version_info < (3, 8):
  74. try:
  75. os.remove(path)
  76. except FileNotFoundError:
  77. pass
  78. else:
  79. path.unlink(missing_ok=True)
  80. def clean_up(paths):
  81. for path in paths:
  82. remove_if_exists(path)
  83. self._finalizer = weakref.finalize(
  84. self, clean_up, [Path(self.rcfile_psmerc), Path(self.rcfile_wgs84)]
  85. )
  86. def add_vector(self, name):
  87. """Imports vector into temporary WGS84 location,
  88. re-formats to a GeoJSON and adds to folium map.
  89. :param str name: name of vector to be added to map;
  90. positional-only parameter
  91. """
  92. # Find full name of vector
  93. file_info = gs.find_file(name, element="vector")
  94. full_name = file_info["fullname"]
  95. name = file_info["name"]
  96. mapset = file_info["mapset"]
  97. new_name = full_name.replace("@", "_")
  98. # Reproject vector into WGS84 Location
  99. env_info = gs.gisenv(env=self._src_env)
  100. gs.run_command(
  101. "v.proj",
  102. input=name,
  103. output=new_name,
  104. mapset=mapset,
  105. location=env_info["LOCATION_NAME"],
  106. dbase=env_info["GISDBASE"],
  107. env=self._wgs84_env,
  108. )
  109. # Convert to GeoJSON
  110. json_file = Path(self._tmp_dir.name) / f"{new_name}.json"
  111. gs.run_command(
  112. "v.out.ogr",
  113. input=new_name,
  114. output=json_file,
  115. format="GeoJSON",
  116. env=self._wgs84_env,
  117. )
  118. # Import GeoJSON to folium and add to map
  119. self._folium.GeoJson(str(json_file), name=name).add_to(self.map)
  120. def add_raster(self, name, opacity=0.8):
  121. """Imports raster into temporary WGS84 location,
  122. exports as png and overlays on folium map
  123. :param str name: name of raster to add to display; positional-only parameter
  124. :param float opacity: raster opacity, number between
  125. 0 (transparent) and 1 (opaque)
  126. """
  127. # Find full name of raster
  128. file_info = gs.find_file(name, element="cell", env=self._src_env)
  129. full_name = file_info["fullname"]
  130. name = file_info["name"]
  131. mapset = file_info["mapset"]
  132. # Reproject raster into WGS84/epsg3857 location
  133. env_info = gs.gisenv(env=self._src_env)
  134. resolution = estimate_resolution(
  135. raster=name,
  136. mapset=mapset,
  137. location=env_info["LOCATION_NAME"],
  138. dbase=env_info["GISDBASE"],
  139. env=self._psmerc_env,
  140. )
  141. tgt_name = full_name.replace("@", "_")
  142. gs.run_command(
  143. "r.proj",
  144. input=full_name,
  145. output=tgt_name,
  146. mapset=mapset,
  147. location=env_info["LOCATION_NAME"],
  148. dbase=env_info["GISDBASE"],
  149. resolution=resolution,
  150. env=self._psmerc_env,
  151. )
  152. # Write raster to png file with GrassRenderer
  153. region_info = gs.region(env=self._src_env)
  154. png_width = region_info["cols"]
  155. png_height = region_info["rows"]
  156. filename = os.path.join(self._tmp_dir.name, f"{tgt_name}.png")
  157. m = GrassRenderer(
  158. width=png_width,
  159. height=png_height,
  160. env=self._psmerc_env,
  161. filename=filename,
  162. )
  163. m.run("d.rast", map=tgt_name)
  164. # Reproject bounds of raster for overlaying png
  165. # Bounds need to be in WGS84
  166. old_bounds = get_region(self._src_env)
  167. from_proj = get_location_proj_string(env=self._src_env)
  168. to_proj = get_location_proj_string(env=self._wgs84_env)
  169. bounds = reproject_region(old_bounds, from_proj, to_proj)
  170. new_bounds = [
  171. [bounds["north"], bounds["west"]],
  172. [bounds["south"], bounds["east"]],
  173. ]
  174. # Overlay image on folium map
  175. img = self._folium.raster_layers.ImageOverlay(
  176. image=filename,
  177. name=name,
  178. bounds=new_bounds,
  179. opacity=opacity,
  180. interactive=True,
  181. cross_origin=False,
  182. )
  183. # Add image to map
  184. img.add_to(self.map)
  185. def add_layer_control(self, **kwargs):
  186. """Add layer control to display"""
  187. self.layer_control = True
  188. self.layer_control_object = self._folium.LayerControl(**kwargs)
  189. def show(self):
  190. """This function returns a folium figure object with a GRASS raster
  191. overlayed on a basemap.
  192. If map has layer control enabled, additional layers cannot be
  193. added after calling show()."""
  194. if self.layer_control:
  195. self.map.add_child(self.layer_control_object)
  196. # Create Figure
  197. fig = self._folium.Figure(width=self.width, height=self.height)
  198. # Add map to figure
  199. fig.add_child(self.map)
  200. return fig
  201. def save(self, filename):
  202. """Save map as an html map.
  203. :param str filename: name of html file
  204. """
  205. self.map.save(filename)