123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- #
- # AUTHOR(S): Caitlin Haedrich <caitlin DOT haedrich AT gmail>
- #
- # PURPOSE: This module contains functions for 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
- import sys
- import tempfile
- import weakref
- from pathlib import Path
- import grass.script as gs
- from .display import GrassRenderer
- from .region import RegionManagerForInteractiveMap
- from .utils import (
- estimate_resolution,
- get_location_proj_string,
- get_region,
- reproject_region,
- setup_location,
- )
- class InteractiveMap:
- """This class creates interative GRASS maps with folium.
- Basic Usage:
- >>> m = InteractiveMap()
- >>> m.add_vector("streams")
- >>> m.add_raster("elevation")
- >>> m.add_layer_control()
- >>> m.show()
- """
- def __init__(self, width=400, height=400, use_region=False, saved_region=None):
- """Creates a blank folium map centered on g.region.
- :param int height: height in pixels of figure (default 400)
- :param int width: width in pixels of figure (default 400)
- """
- import folium
- self._folium = folium
- # Store height and width
- self.width = width
- self.height = height
- # Make temporary folder for all our files
- self._tmp_dir = tempfile.TemporaryDirectory()
- # Remember original environment; all environments used
- # in this class are derived from this one
- self._src_env = os.environ.copy()
- # Set up temporary locations in WGS84 and Pseudo-Mercator
- # We need two because folium uses WGS84 for vectors and coordinates
- # and Pseudo-Mercator for raster overlays
- self.rcfile_psmerc, self._psmerc_env = setup_location(
- "psmerc", self._tmp_dir.name, "3857", self._src_env
- )
- self.rcfile_wgs84, self._wgs84_env = setup_location(
- "wgs84", self._tmp_dir.name, "4326", self._src_env
- )
- # Get Center of temporary GRASS regions
- center = gs.parse_command("g.region", flags="cg", env=self._wgs84_env)
- center = (float(center["center_northing"]), float(center["center_easting"]))
- # Create Folium Map
- self.map = self._folium.Map(
- width=self.width,
- height=self.height,
- location=center,
- tiles="cartodbpositron",
- )
- # Set LayerControl default
- self.layer_control = False
- # region handling
- self._region_manager = RegionManagerForInteractiveMap(
- use_region, saved_region, self._src_env, self._psmerc_env
- )
- # Cleanup rcfiles with finalizer
- def remove_if_exists(path):
- if sys.version_info < (3, 8):
- try:
- os.remove(path)
- except FileNotFoundError:
- pass
- else:
- path.unlink(missing_ok=True)
- def clean_up(paths):
- for path in paths:
- remove_if_exists(path)
- self._finalizer = weakref.finalize(
- self, clean_up, [Path(self.rcfile_psmerc), Path(self.rcfile_wgs84)]
- )
- def add_vector(self, name):
- """Imports vector into temporary WGS84 location,
- re-formats to a GeoJSON and adds to folium map.
- :param str name: name of vector to be added to map;
- positional-only parameter
- """
- # Find full name of vector
- file_info = gs.find_file(name, element="vector")
- full_name = file_info["fullname"]
- name = file_info["name"]
- mapset = file_info["mapset"]
- new_name = full_name.replace("@", "_")
- # set bbox
- self._region_manager.set_bbox_vector(full_name)
- # Reproject vector into WGS84 Location
- env_info = gs.gisenv(env=self._src_env)
- gs.run_command(
- "v.proj",
- input=name,
- output=new_name,
- mapset=mapset,
- location=env_info["LOCATION_NAME"],
- dbase=env_info["GISDBASE"],
- env=self._wgs84_env,
- )
- # Convert to GeoJSON
- json_file = Path(self._tmp_dir.name) / f"{new_name}.json"
- gs.run_command(
- "v.out.ogr",
- input=new_name,
- output=json_file,
- format="GeoJSON",
- env=self._wgs84_env,
- )
- # Import GeoJSON to folium and add to map
- self._folium.GeoJson(str(json_file), name=name).add_to(self.map)
- def add_raster(self, name, opacity=0.8):
- """Imports raster into temporary WGS84 location,
- exports as png and overlays on folium map.
- Color table for the raster can be modified with `r.colors` before calling
- this function.
- .. note:: This will only work if the raster is located in the current mapset.
- To change the color table of a raster located outside the current mapset,
- switch to that mapset with `g.mapset`, modify the color table with `r.color`
- then switch back to the initial mapset and run this function.
- :param str name: name of raster to add to display; positional-only parameter
- :param float opacity: raster opacity, number between
- 0 (transparent) and 1 (opaque)
- """
- # Find full name of raster
- file_info = gs.find_file(name, element="cell", env=self._src_env)
- full_name = file_info["fullname"]
- name = file_info["name"]
- mapset = file_info["mapset"]
- self._region_manager.set_region_from_raster(full_name)
- # Reproject raster into WGS84/epsg3857 location
- env_info = gs.gisenv(env=self._src_env)
- resolution = estimate_resolution(
- raster=name,
- mapset=mapset,
- location=env_info["LOCATION_NAME"],
- dbase=env_info["GISDBASE"],
- env=self._psmerc_env,
- )
- tgt_name = full_name.replace("@", "_")
- gs.run_command(
- "r.proj",
- input=full_name,
- output=tgt_name,
- mapset=mapset,
- location=env_info["LOCATION_NAME"],
- dbase=env_info["GISDBASE"],
- resolution=resolution,
- env=self._psmerc_env,
- )
- # Write raster to png file with GrassRenderer
- region_info = gs.region(env=self._src_env)
- png_width = region_info["cols"]
- png_height = region_info["rows"]
- filename = os.path.join(self._tmp_dir.name, f"{tgt_name}.png")
- m = GrassRenderer(
- width=png_width,
- height=png_height,
- env=self._psmerc_env,
- filename=filename,
- use_region=True,
- )
- m.run("d.rast", map=tgt_name)
- # Reproject bounds of raster for overlaying png
- # Bounds need to be in WGS84
- old_bounds = get_region(self._src_env)
- from_proj = get_location_proj_string(env=self._src_env)
- to_proj = get_location_proj_string(env=self._wgs84_env)
- bounds = reproject_region(old_bounds, from_proj, to_proj)
- new_bounds = [
- [bounds["north"], bounds["west"]],
- [bounds["south"], bounds["east"]],
- ]
- # Overlay image on folium map
- img = self._folium.raster_layers.ImageOverlay(
- image=filename,
- name=name,
- bounds=new_bounds,
- opacity=opacity,
- interactive=True,
- cross_origin=False,
- )
- # Add image to map
- img.add_to(self.map)
- def add_layer_control(self, **kwargs):
- """Add layer control to display"
- :param `**kwargs`: named arguments to be passed to folium.LayerControl()"""
- self.layer_control = True
- self.layer_control_object = self._folium.LayerControl(**kwargs)
- def show(self):
- """This function returns a folium figure object with a GRASS raster
- overlayed on a basemap.
- If map has layer control enabled, additional layers cannot be
- added after calling show()."""
- if self.layer_control:
- self.map.add_child(self.layer_control_object)
- # Create Figure
- fig = self._folium.Figure(width=self.width, height=self.height)
- # Add map to figure
- fig.add_child(self.map)
- self.map.fit_bounds(self._region_manager.bbox)
- return fig
- def save(self, filename):
- """Save map as an html map.
- :param str filename: name of html file
- """
- self.map.save(filename)
|