-
-
Notifications
You must be signed in to change notification settings - Fork 286
/
map.py
198 lines (161 loc) · 6.74 KB
/
map.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# 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
import grass.script as gs
from .region import RegionManagerFor2D
class Map:
"""Map creates and displays GRASS maps in
Jupyter Notebooks.
Elements are added to the display by calling GRASS display modules.
Basic usage::
>>> 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::
>>> 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,
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: environment
: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()
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 os.path.exists(self._filename):
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,
)
@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
def run(self, module, **kwargs):
"""Run modules from the GRASS display family (modules starting with "d.").
This function passes arguments directly to grass.script.run_command()
so the syntax is the same.
:param str module: name of GRASS module
:param `**kwargs`: named arguments passed to run_command()"""
# Check module is from display library then run
if module[0] == "d":
self._region_manager.set_region_from_command(module, **kwargs)
self._region_manager.adjust_rendering_size_from_region()
gs.run_command(module, env=self._env, **kwargs)
else:
raise ValueError("Module must begin with letter 'd'.")
def __getattr__(self, name):
"""Parse attribute to GRASS display module. Attribute should be in
the form 'd_module_name'. For example, 'd.rast' is called with 'd_rast'.
"""
# Check to make sure format is correct
if not name.startswith("d_"):
raise AttributeError(_("Module must begin with 'd_'"))
# Reformat string
grass_module = name.replace("_", ".")
# Assert module exists
if not shutil.which(grass_module):
raise AttributeError(_("Cannot find GRASS module {}").format(grass_module))
def wrapper(**kwargs):
# Run module
self.run(grass_module, **kwargs)
return wrapper
def show(self):
"""Displays a PNG image of map"""
# Lazy import to avoid an import-time dependency on IPython.
from IPython.display import Image # pylint: disable=import-outside-toplevel
return Image(self._filename)
def save(self, filename):
"""Saves a PNG image of map to the specified *filename*"""
shutil.copy(self._filename, filename)