Grass7Utils.py

Debugging Grass7Utils.py - Médéric RIBREUX, 2018-01-07 11:48 AM

Download (21.2 KB)

 
1
# -*- coding: utf-8 -*-
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
# This will get replaced with a git SHA1 when you do a git archive
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
    # TODO Review all default options formats
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
        # Launch GRASS command with -v parameter
101
        # For MS-Windows, hide the console
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
        # For MS-Windows there is a difference between GRASS Path and GRASS binary
159
        if isWindows():
160
            # If nothing found, use OSGEO4W or QgsPrefix:
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
            # Search in grassPath
169
            command = searchFolder(path)
170

    
171
        # Under GNU/Linux or if everything has failed, use shutil
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
            # Under MS-Windows, we use OSGEO4W or QGIS Path for folder
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
                # For MacOSX, we scan some well-known directories
217
                # Start with QGIS bundle
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
                    # If nothing found, try standalone GRASS installation
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
        #QgsMessageLog.logMessage('exec: {}'.format(command), 'DEBUG', QgsMessageLog.INFO)
355

    
356
        # For MS-Windows, we need to hide the console window.
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
        # Some GRASS scripts, like r.mapcalculator or r.fillnulls, call
385
        # other GRASS scripts during execution. This may override any
386
        # commands that are still to be executed by the subprocess, which
387
        # are usually the output ones. If that is the case runs the output
388
        # commands again.
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
    # GRASS session is used to hold the layers already exported or
416
    # produced in GRASS between multiple calls to GRASS algorithms.
417
    # This way they don't have to be loaded multiple times and
418
    # following algorithms can use the results of the previous ones.
419
    # Starting a session just involves creating the temp mapset
420
    # structure
421
    @staticmethod
422
    def startGrassSession():
423
        if not Grass7Utils.sessionRunning:
424
            Grass7Utils.createTempMapset()
425
            Grass7Utils.sessionRunning = True
426

    
427
    # End session by removing the temporary GRASS mapset and all
428
    # the layers.
429
    @staticmethod
430
    def endGrassSession():
431
        #shutil.rmtree(Grass7Utils.grassMapsetFolder(), True)
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
        # We check the version of Grass7
453
        if Grass7Utils.installedVersion() is not None:
454
            # For Ms-Windows, we check GRASS binaries
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
        # Return error messages
466
        else:
467
            # MS-Windows or MacOSX
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
            # GNU/Linux
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
            # Python 2
495
            output.write(command.encode('utf8') + '\n')
496
        except TypeError:
497
            # Python 3
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
            # GRASS not available!
526
            return 'https://grass.osgeo.org/grass72/manuals/'
527

    
528
    @staticmethod
529
    def getSupportedOutputRasterExtensions():
530
        # We use the same extensions than GDAL because:
531
        # - GRASS is also using GDAL for raster imports.
532
        # - Chances that GRASS is compiled with another version of
533
        # GDAL than QGIS are very limited!
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'