1
|
|
2
|
|
3
|
"""
|
4
|
***************************************************************************
|
5
|
GrassUtils.py
|
6
|
---------------------
|
7
|
Date : February 2015
|
8
|
Copyright : (C) 2014-2015 by Victor Olaya
|
9
|
Email : volayaf at gmail dot com
|
10
|
***************************************************************************
|
11
|
* *
|
12
|
* This program is free software; you can redistribute it and/or modify *
|
13
|
* it under the terms of the GNU General Public License as published by *
|
14
|
* the Free Software Foundation; either version 2 of the License, or *
|
15
|
* (at your option) any later version. *
|
16
|
* *
|
17
|
***************************************************************************
|
18
|
"""
|
19
|
|
20
|
__author__ = 'Victor Olaya'
|
21
|
__date__ = 'February 2015'
|
22
|
__copyright__ = '(C) 2014-2015, Victor Olaya'
|
23
|
|
24
|
|
25
|
|
26
|
__revision__ = '$Format:%H$'
|
27
|
|
28
|
import stat
|
29
|
import shutil
|
30
|
import shlex
|
31
|
import subprocess
|
32
|
import os
|
33
|
|
34
|
from qgis.core import (QgsApplication,
|
35
|
QgsProcessingUtils,
|
36
|
QgsMessageLog)
|
37
|
from qgis.PyQt.QtCore import QCoreApplication
|
38
|
from processing.core.ProcessingConfig import ProcessingConfig
|
39
|
from processing.tools.system import userFolder, isWindows, isMac, mkdir
|
40
|
from processing.tests.TestData import points
|
41
|
from processing.algs.gdal.GdalUtils import GdalUtils
|
42
|
|
43
|
|
44
|
class Grass7Utils:
|
45
|
|
46
|
GRASS_REGION_XMIN = 'GRASS7_REGION_XMIN'
|
47
|
GRASS_REGION_YMIN = 'GRASS7_REGION_YMIN'
|
48
|
GRASS_REGION_XMAX = 'GRASS7_REGION_XMAX'
|
49
|
GRASS_REGION_YMAX = 'GRASS7_REGION_YMAX'
|
50
|
GRASS_REGION_CELLSIZE = 'GRASS7_REGION_CELLSIZE'
|
51
|
GRASS_FOLDER = 'GRASS7_FOLDER'
|
52
|
GRASS_LOG_COMMANDS = 'GRASS7_LOG_COMMANDS'
|
53
|
GRASS_LOG_CONSOLE = 'GRASS7_LOG_CONSOLE'
|
54
|
GRASS_HELP_PATH = 'GRASS_HELP_PATH'
|
55
|
GRASS_USE_VEXTERNAL = 'GRASS_USE_VEXTERNAL'
|
56
|
|
57
|
|
58
|
GRASS_RASTER_FORMATS_CREATEOPTS = {
|
59
|
'GTiff': 'TFW=YES,COMPRESS=LZW',
|
60
|
'PNG': 'ZLEVEL=9',
|
61
|
'WEBP': 'QUALITY=85'
|
62
|
}
|
63
|
|
64
|
sessionRunning = False
|
65
|
sessionLayers = {}
|
66
|
projectionSet = False
|
67
|
|
68
|
isGrassInstalled = False
|
69
|
|
70
|
version = None
|
71
|
path = None
|
72
|
command = None
|
73
|
|
74
|
@staticmethod
|
75
|
def grassBatchJobFilename():
|
76
|
"""
|
77
|
The Batch file is executed by GRASS binary.
|
78
|
On GNU/Linux and MacOSX it will be executed by a shell.
|
79
|
On MS-Windows, it will be executed by cmd.exe.
|
80
|
"""
|
81
|
gisdbase = Grass7Utils.grassDataFolder()
|
82
|
if isWindows():
|
83
|
batchFile = os.path.join(gisdbase, 'grass_batch_job.cmd')
|
84
|
else:
|
85
|
batchFile = os.path.join(gisdbase, 'grass_batch_job.sh')
|
86
|
return batchFile
|
87
|
|
88
|
@staticmethod
|
89
|
def installedVersion(run=False):
|
90
|
"""
|
91
|
Returns the installed version of GRASS by
|
92
|
launching the GRASS command with -v parameter.
|
93
|
"""
|
94
|
if Grass7Utils.isGrassInstalled and not run:
|
95
|
return Grass7Utils.version
|
96
|
|
97
|
if Grass7Utils.grassBin() is None:
|
98
|
return None
|
99
|
|
100
|
|
101
|
|
102
|
if isWindows():
|
103
|
si = subprocess.STARTUPINFO()
|
104
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
105
|
si.wShowWindow = subprocess.SW_HIDE
|
106
|
with subprocess.Popen(
|
107
|
[Grass7Utils.command, '-v'],
|
108
|
shell=True if isMac() else False,
|
109
|
stdout=subprocess.PIPE,
|
110
|
stdin=subprocess.DEVNULL,
|
111
|
stderr=subprocess.STDOUT,
|
112
|
universal_newlines=True,
|
113
|
startupinfo=si if isWindows() else None
|
114
|
) as proc:
|
115
|
try:
|
116
|
lines = proc.stdout.readlines()
|
117
|
for line in lines:
|
118
|
QgsMessageLog.logMessage('installedVersion: {}'.format(line), 'DEBUG', QgsMessageLog.INFO)
|
119
|
if "GRASS GIS " in line:
|
120
|
line = line.split(" ")[-1].strip()
|
121
|
if line.startswith("7."):
|
122
|
Grass7Utils.version = line
|
123
|
return Grass7Utils.version
|
124
|
except:
|
125
|
pass
|
126
|
|
127
|
return None
|
128
|
|
129
|
@staticmethod
|
130
|
def grassBin():
|
131
|
"""
|
132
|
Find GRASS binary path on the operating system.
|
133
|
Sets global variable Grass7Utils.command
|
134
|
"""
|
135
|
cmdList = ["grass74", "grass72", "grass71", "grass70", "grass",
|
136
|
"grass74.sh", "grass72.sh", "grass71.sh", "grass70.sh", "grass.sh"]
|
137
|
|
138
|
def searchFolder(folder):
|
139
|
"""
|
140
|
Inline function to search for grass binaries into a folder
|
141
|
with os.walk
|
142
|
"""
|
143
|
command = None
|
144
|
if os.path.exists(folder):
|
145
|
for root, dirs, files in os.walk(folder):
|
146
|
for cmd in cmdList:
|
147
|
if cmd in files:
|
148
|
command = os.path.join(root, cmd)
|
149
|
break
|
150
|
return command
|
151
|
|
152
|
if Grass7Utils.command:
|
153
|
return Grass7Utils.command
|
154
|
|
155
|
path = Grass7Utils.grassPath()
|
156
|
command = None
|
157
|
|
158
|
|
159
|
if isWindows():
|
160
|
|
161
|
if "OSGEO4W_ROOT" in os.environ:
|
162
|
testFolder = str(os.environ['OSGEO4W_ROOT'])
|
163
|
else:
|
164
|
testFolder = str(QgsApplication.prefixPath())
|
165
|
testFolder = os.path.join(testFolder, 'bin')
|
166
|
command = searchFolder(testFolder)
|
167
|
elif isMac():
|
168
|
|
169
|
command = searchFolder(path)
|
170
|
|
171
|
|
172
|
if not command:
|
173
|
for cmd in cmdList:
|
174
|
testBin = shutil.which(cmd)
|
175
|
if testBin:
|
176
|
command = os.path.abspath(testBin)
|
177
|
break
|
178
|
|
179
|
if command:
|
180
|
Grass7Utils.command = command
|
181
|
if path is '':
|
182
|
Grass7Utils.path = os.path.dirname(command)
|
183
|
QgsMessageLog.logMessage('grassBin: command:{}, path:{}'.format(command, path), 'DEBUG', QgsMessageLog.INFO)
|
184
|
return command
|
185
|
|
186
|
@staticmethod
|
187
|
def grassPath():
|
188
|
"""
|
189
|
Find GRASS path on the operating system.
|
190
|
Sets global variable Grass7Utils.path
|
191
|
"""
|
192
|
if Grass7Utils.path is not None:
|
193
|
return Grass7Utils.path
|
194
|
|
195
|
if not isWindows() and not isMac():
|
196
|
return ''
|
197
|
|
198
|
folder = ProcessingConfig.getSetting(Grass7Utils.GRASS_FOLDER) or ''
|
199
|
if not os.path.exists(folder):
|
200
|
folder = None
|
201
|
|
202
|
if folder is None:
|
203
|
|
204
|
if isWindows():
|
205
|
if "OSGEO4W_ROOT" in os.environ:
|
206
|
testfolder = os.path.join(str(os.environ['OSGEO4W_ROOT']), "apps")
|
207
|
else:
|
208
|
testfolder = str(QgsApplication.prefixPath())
|
209
|
testfolder = os.path.join(testfolder, 'grass')
|
210
|
if os.path.isdir(testfolder):
|
211
|
for subfolder in os.listdir(testfolder):
|
212
|
if subfolder.startswith('grass-7'):
|
213
|
folder = os.path.join(testfolder, subfolder)
|
214
|
break
|
215
|
elif isMac():
|
216
|
|
217
|
|
218
|
for version in ['', '7', '70', '71', '72', '74']:
|
219
|
testfolder = os.path.join(str(QgsApplication.prefixPath()),
|
220
|
'grass{}'.format(version))
|
221
|
if os.path.isdir(testfolder):
|
222
|
folder = testfolder
|
223
|
break
|
224
|
|
225
|
if folder is None:
|
226
|
for version in ['0', '1', '2', '4']:
|
227
|
testfolder = '/Applications/GRASS-7.{}.app/Contents/MacOS'.format(version)
|
228
|
if os.path.isdir(testfolder):
|
229
|
folder = testfolder
|
230
|
break
|
231
|
|
232
|
if folder is not None:
|
233
|
Grass7Utils.path = folder
|
234
|
|
235
|
QgsMessageLog.logMessage('grassPath: {}'.format(folder), 'DEBUG', QgsMessageLog.INFO)
|
236
|
return folder or ''
|
237
|
|
238
|
@staticmethod
|
239
|
def grassDescriptionPath():
|
240
|
return os.path.join(os.path.dirname(__file__), 'description')
|
241
|
|
242
|
@staticmethod
|
243
|
def getWindowsCodePage():
|
244
|
"""
|
245
|
Determines MS-Windows CMD.exe shell codepage.
|
246
|
Used into GRASS exec script under MS-Windows.
|
247
|
"""
|
248
|
from ctypes import cdll
|
249
|
return str(cdll.kernel32.GetACP())
|
250
|
|
251
|
@staticmethod
|
252
|
def createGrassBatchJobFileFromGrassCommands(commands):
|
253
|
with open(Grass7Utils.grassBatchJobFilename(), 'w') as fout:
|
254
|
if not isWindows():
|
255
|
fout.write('#!/bin/sh\n')
|
256
|
else:
|
257
|
fout.write('chcp {}>NUL\n'.format(Grass7Utils.getWindowsCodePage()))
|
258
|
for command in commands:
|
259
|
Grass7Utils.writeCommand(fout, command)
|
260
|
fout.write('exit')
|
261
|
|
262
|
@staticmethod
|
263
|
def grassMapsetFolder():
|
264
|
"""
|
265
|
Creates and returns the GRASS temporary DB LOCATION directory.
|
266
|
"""
|
267
|
folder = os.path.join(Grass7Utils.grassDataFolder(), 'temp_location')
|
268
|
mkdir(folder)
|
269
|
return folder
|
270
|
|
271
|
@staticmethod
|
272
|
def grassDataFolder():
|
273
|
"""
|
274
|
Creates and returns the GRASS temporary DB directory.
|
275
|
"""
|
276
|
tempfolder = os.path.normpath(
|
277
|
os.path.join(QgsProcessingUtils.tempFolder(), 'grassdata'))
|
278
|
mkdir(tempfolder)
|
279
|
return tempfolder
|
280
|
|
281
|
@staticmethod
|
282
|
def createTempMapset():
|
283
|
"""
|
284
|
Creates a temporary location and mapset(s) for GRASS data
|
285
|
processing. A minimal set of folders and files is created in the
|
286
|
system's default temporary directory. The settings files are
|
287
|
written with sane defaults, so GRASS can do its work. The mapset
|
288
|
projection will be set later, based on the projection of the first
|
289
|
input image or vector
|
290
|
"""
|
291
|
folder = Grass7Utils.grassMapsetFolder()
|
292
|
mkdir(os.path.join(folder, 'PERMANENT'))
|
293
|
mkdir(os.path.join(folder, 'PERMANENT', '.tmp'))
|
294
|
Grass7Utils.writeGrassWindow(os.path.join(folder, 'PERMANENT', 'DEFAULT_WIND'))
|
295
|
with open(os.path.join(folder, 'PERMANENT', 'MYNAME'), 'w') as outfile:
|
296
|
outfile.write(
|
297
|
'QGIS GRASS GIS 7 interface: temporary data processing location.\n')
|
298
|
|
299
|
Grass7Utils.writeGrassWindow(os.path.join(folder, 'PERMANENT', 'WIND'))
|
300
|
mkdir(os.path.join(folder, 'PERMANENT', 'sqlite'))
|
301
|
with open(os.path.join(folder, 'PERMANENT', 'VAR'), 'w') as outfile:
|
302
|
outfile.write('DB_DRIVER: sqlite\n')
|
303
|
outfile.write('DB_DATABASE: $GISDBASE/$LOCATION_NAME/$MAPSET/sqlite/sqlite.db\n')
|
304
|
|
305
|
@staticmethod
|
306
|
def writeGrassWindow(filename):
|
307
|
"""
|
308
|
Creates the GRASS Window file
|
309
|
"""
|
310
|
with open(filename, 'w') as out:
|
311
|
out.write('proj: 0\n')
|
312
|
out.write('zone: 0\n')
|
313
|
out.write('north: 1\n')
|
314
|
out.write('south: 0\n')
|
315
|
out.write('east: 1\n')
|
316
|
out.write('west: 0\n')
|
317
|
out.write('cols: 1\n')
|
318
|
out.write('rows: 1\n')
|
319
|
out.write('e-w resol: 1\n')
|
320
|
out.write('n-s resol: 1\n')
|
321
|
out.write('top: 1\n')
|
322
|
out.write('bottom: 0\n')
|
323
|
out.write('cols3: 1\n')
|
324
|
out.write('rows3: 1\n')
|
325
|
out.write('depths: 1\n')
|
326
|
out.write('e-w resol3: 1\n')
|
327
|
out.write('n-s resol3: 1\n')
|
328
|
out.write('t-b resol: 1\n')
|
329
|
|
330
|
@staticmethod
|
331
|
def prepareGrassExecution(commands):
|
332
|
"""
|
333
|
Prepare GRASS batch job in a script and
|
334
|
returns it as a command ready for subprocess.
|
335
|
"""
|
336
|
env = os.environ.copy()
|
337
|
env['GRASS_MESSAGE_FORMAT'] = 'plain'
|
338
|
if 'GISBASE' in env:
|
339
|
del env['GISBASE']
|
340
|
Grass7Utils.createGrassBatchJobFileFromGrassCommands(commands)
|
341
|
os.chmod(Grass7Utils.grassBatchJobFilename(), stat.S_IEXEC | stat.S_IREAD | stat.S_IWRITE)
|
342
|
command = [Grass7Utils.command,
|
343
|
os.path.join(Grass7Utils.grassMapsetFolder(), 'PERMANENT'),
|
344
|
'--exec', Grass7Utils.grassBatchJobFilename()]
|
345
|
|
346
|
return command, env
|
347
|
|
348
|
@staticmethod
|
349
|
def executeGrass(commands, feedback, outputCommands=None):
|
350
|
loglines = []
|
351
|
loglines.append(Grass7Utils.tr('GRASS GIS 7 execution console output'))
|
352
|
grassOutDone = False
|
353
|
command, grassenv = Grass7Utils.prepareGrassExecution(commands)
|
354
|
|
355
|
|
356
|
|
357
|
if isWindows():
|
358
|
si = subprocess.STARTUPINFO()
|
359
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
360
|
si.wShowWindow = subprocess.SW_HIDE
|
361
|
QgsMessageLog.logMessage('executeGrass: command:{}\n grassenv:{}\n'.format(command, grassenv), 'DEBUG', QgsMessageLog.INFO)
|
362
|
with subprocess.Popen(
|
363
|
command,
|
364
|
shell=True if isMac() else False,
|
365
|
stdout=subprocess.PIPE,
|
366
|
stdin=subprocess.DEVNULL,
|
367
|
stderr=subprocess.STDOUT,
|
368
|
universal_newlines=True,
|
369
|
env=grassenv,
|
370
|
startupinfo=si if isWindows() else None
|
371
|
) as proc:
|
372
|
for line in iter(proc.stdout.readline, ''):
|
373
|
if 'GRASS_INFO_PERCENT' in line:
|
374
|
try:
|
375
|
feedback.setProgress(int(line[len('GRASS_INFO_PERCENT') + 2:]))
|
376
|
except:
|
377
|
pass
|
378
|
else:
|
379
|
if 'r.out' in line or 'v.out' in line:
|
380
|
grassOutDone = True
|
381
|
loglines.append(line)
|
382
|
feedback.pushConsoleInfo(line)
|
383
|
|
384
|
|
385
|
|
386
|
|
387
|
|
388
|
|
389
|
if not grassOutDone and outputCommands:
|
390
|
command, grassenv = Grass7Utils.prepareGrassExecution(outputCommands)
|
391
|
with subprocess.Popen(
|
392
|
command,
|
393
|
shell=True if isMac() else False,
|
394
|
stdout=subprocess.PIPE,
|
395
|
stdin=subprocess.DEVNULL,
|
396
|
stderr=subprocess.STDOUT,
|
397
|
universal_newlines=True,
|
398
|
env=grassenv,
|
399
|
startupinfo=si if isWindows() else None
|
400
|
) as proc:
|
401
|
for line in iter(proc.stdout.readline, ''):
|
402
|
if 'GRASS_INFO_PERCENT' in line:
|
403
|
try:
|
404
|
feedback.setProgress(int(
|
405
|
line[len('GRASS_INFO_PERCENT') + 2:]))
|
406
|
except:
|
407
|
pass
|
408
|
else:
|
409
|
loglines.append(line)
|
410
|
feedback.pushConsoleInfo(line)
|
411
|
|
412
|
if ProcessingConfig.getSetting(Grass7Utils.GRASS_LOG_CONSOLE):
|
413
|
QgsMessageLog.logMessage('\n'.join(loglines), 'Processing', QgsMessageLog.INFO)
|
414
|
|
415
|
|
416
|
|
417
|
|
418
|
|
419
|
|
420
|
|
421
|
@staticmethod
|
422
|
def startGrassSession():
|
423
|
if not Grass7Utils.sessionRunning:
|
424
|
Grass7Utils.createTempMapset()
|
425
|
Grass7Utils.sessionRunning = True
|
426
|
|
427
|
|
428
|
|
429
|
@staticmethod
|
430
|
def endGrassSession():
|
431
|
|
432
|
Grass7Utils.sessionRunning = False
|
433
|
Grass7Utils.sessionLayers = {}
|
434
|
Grass7Utils.projectionSet = False
|
435
|
|
436
|
@staticmethod
|
437
|
def getSessionLayers():
|
438
|
return Grass7Utils.sessionLayers
|
439
|
|
440
|
@staticmethod
|
441
|
def addSessionLayers(exportedLayers):
|
442
|
Grass7Utils.sessionLayers = dict(
|
443
|
list(Grass7Utils.sessionLayers.items()) +
|
444
|
list(exportedLayers.items()))
|
445
|
|
446
|
@staticmethod
|
447
|
def checkGrassIsInstalled(ignorePreviousState=False):
|
448
|
if not ignorePreviousState:
|
449
|
if Grass7Utils.isGrassInstalled:
|
450
|
return
|
451
|
|
452
|
|
453
|
if Grass7Utils.installedVersion() is not None:
|
454
|
|
455
|
if isWindows():
|
456
|
cmdpath = os.path.join(Grass7Utils.path, 'bin', 'r.out.gdal.exe')
|
457
|
if not os.path.exists(cmdpath):
|
458
|
return Grass7Utils.tr(
|
459
|
'The specified GRASS 7 folder "{}" does not contain '
|
460
|
'a valid set of GRASS 7 modules.\nPlease, go to the '
|
461
|
'Processing settings dialog, and check that the '
|
462
|
'GRASS 7\nfolder is correctly configured'.format(os.path.join(path, 'bin')))
|
463
|
Grass7Utils.isGrassInstalled = True
|
464
|
return
|
465
|
|
466
|
else:
|
467
|
|
468
|
if isWindows() or isMac():
|
469
|
if Grass7Utils.path is None:
|
470
|
return Grass7Utils.tr(
|
471
|
'GRASS GIS 7 folder is not configured. Please configure '
|
472
|
'it before running GRASS GIS 7 algorithms.')
|
473
|
if Grass7Utils.command is None:
|
474
|
return Grass7Utils.tr(
|
475
|
'GRASS GIS 7 binary {0} can\'t be found on this system from a shell. '
|
476
|
'Please install it or configure your PATH {1} environment variable.'.format(
|
477
|
'(grass.bat)' if isWindows() else '(grass.sh)',
|
478
|
'or OSGEO4W_ROOT' if isWindows() else ''))
|
479
|
|
480
|
else:
|
481
|
return Grass7Utils.tr(
|
482
|
'GRASS 7 can\'t be found on this system from a shell. '
|
483
|
'Please install it or configure your PATH environment variable.')
|
484
|
|
485
|
@staticmethod
|
486
|
def tr(string, context=''):
|
487
|
if context == '':
|
488
|
context = 'Grass7Utils'
|
489
|
return QCoreApplication.translate(context, string)
|
490
|
|
491
|
@staticmethod
|
492
|
def writeCommand(output, command):
|
493
|
try:
|
494
|
|
495
|
output.write(command.encode('utf8') + '\n')
|
496
|
except TypeError:
|
497
|
|
498
|
output.write(command + '\n')
|
499
|
|
500
|
@staticmethod
|
501
|
def grassHelpPath():
|
502
|
helpPath = ProcessingConfig.getSetting(Grass7Utils.GRASS_HELP_PATH)
|
503
|
|
504
|
if helpPath is None:
|
505
|
if isWindows() or isMac():
|
506
|
if Grass7Utils.path is not None:
|
507
|
localPath = os.path.join(Grass7Utils.path, 'docs/html')
|
508
|
if os.path.exists(localPath):
|
509
|
helpPath = os.path.abspath(localPath)
|
510
|
else:
|
511
|
searchPaths = ['/usr/share/doc/grass-doc/html',
|
512
|
'/opt/grass/docs/html',
|
513
|
'/usr/share/doc/grass/docs/html']
|
514
|
for path in searchPaths:
|
515
|
if os.path.exists(path):
|
516
|
helpPath = os.path.abspath(path)
|
517
|
break
|
518
|
|
519
|
if helpPath is not None:
|
520
|
return helpPath
|
521
|
elif Grass7Utils.version:
|
522
|
version = Grass7Utils.version.replace('.', '')[:2]
|
523
|
return 'https://grass.osgeo.org/grass{}/manuals/'.format(version)
|
524
|
else:
|
525
|
|
526
|
return 'https://grass.osgeo.org/grass72/manuals/'
|
527
|
|
528
|
@staticmethod
|
529
|
def getSupportedOutputRasterExtensions():
|
530
|
|
531
|
|
532
|
|
533
|
|
534
|
return GdalUtils.getSupportedOutputRasterExtensions()
|
535
|
|
536
|
@staticmethod
|
537
|
def getRasterFormatFromFilename(filename):
|
538
|
"""
|
539
|
Returns Raster format name from a raster filename.
|
540
|
:param filename: The name with extension of the raster.
|
541
|
:return: The Gdal short format name for extension.
|
542
|
"""
|
543
|
ext = os.path.splitext(filename)[1].lower()
|
544
|
ext = ext.lstrip('.')
|
545
|
if ext:
|
546
|
supported = GdalUtils.getSupportedRasters()
|
547
|
for name in list(supported.keys()):
|
548
|
exts = supported[name]
|
549
|
if ext in exts:
|
550
|
return name
|
551
|
return 'GTiff'
|