Grass7Utils.py

Médéric RIBREUX, 2016-03-13 10:26 AM

Download (17.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 subprocess
31
import os
32
import sys
33
from qgis.core import QgsApplication
34
from PyQt4.QtCore import QCoreApplication
35
from processing.core.ProcessingConfig import ProcessingConfig
36
from processing.core.ProcessingLog import ProcessingLog
37
from processing.tools.system import userFolder, isWindows, isMac, tempFolder, mkdir, getTempFilenameInTempFolder
38
from processing.tests.TestData import points
39

    
40

    
41
class Grass7Utils:
42

    
43
    GRASS_REGION_XMIN = 'GRASS7_REGION_XMIN'
44
    GRASS_REGION_YMIN = 'GRASS7_REGION_YMIN'
45
    GRASS_REGION_XMAX = 'GRASS7_REGION_XMAX'
46
    GRASS_REGION_YMAX = 'GRASS7_REGION_YMAX'
47
    GRASS_REGION_CELLSIZE = 'GRASS7_REGION_CELLSIZE'
48
    GRASS_FOLDER = 'GRASS7_FOLDER'
49
    GRASS_WIN_SHELL = 'GRASS7_WIN_SHELL'
50
    GRASS_LOG_COMMANDS = 'GRASS7_LOG_COMMANDS'
51
    GRASS_LOG_CONSOLE = 'GRASS7_LOG_CONSOLE'
52

    
53
    sessionRunning = False
54
    sessionLayers = {}
55
    projectionSet = False
56

    
57
    isGrass7Installed = False
58

    
59
    @staticmethod
60
    def grassBatchJobFilename():
61
        '''This is used in Linux. This is the batch job that we assign to
62
        GRASS_BATCH_JOB and then call GRASS and let it do the work
63
        '''
64
        filename = 'grass7_batch_job.sh'
65
        batchfile = os.path.join(userFolder(), filename)
66
        return batchfile
67

    
68
    @staticmethod
69
    def grassScriptFilename():
70
        '''This is used in windows. We create a script that initializes
71
        GRASS and then uses grass commands
72
        '''
73
        filename = 'grass7_script.bat'
74
        filename = os.path.join(userFolder(), filename)
75
        return filename
76

    
77
    @staticmethod
78
    def getGrassVersion():
79
        # FIXME: I do not know if this should be removed or let the user enter it
80
        # or something like that... This is just a temporary thing
81
        return '7.0.0'
82

    
83
    @staticmethod
84
    def grassPath():
85
        if not isWindows() and not isMac():
86
            return ''
87

    
88
        folder = ProcessingConfig.getSetting(Grass7Utils.GRASS_FOLDER)
89
        if folder is None:
90
            if isWindows():
91
                testfolder = os.path.dirname(unicode(QgsApplication.prefixPath()))
92
                testfolder = os.path.join(testfolder, 'grass')
93
                if os.path.isdir(testfolder):
94
                    for subfolder in os.listdir(testfolder):
95
                        if subfolder.startswith('grass-7'):
96
                            folder = os.path.join(testfolder, subfolder)
97
                            break
98
            else:
99
                folder = os.path.join(unicode(QgsApplication.prefixPath()), 'grass7')
100
                if not os.path.isdir(folder):
101
                    folder = '/Applications/GRASS-7.0.app/Contents/MacOS'
102

    
103
        return folder
104

    
105
    @staticmethod
106
    def grassWinShell():
107
        folder = ProcessingConfig.getSetting(Grass7Utils.GRASS_WIN_SHELL)
108
        if folder is None:
109
            folder = os.path.dirname(unicode(QgsApplication.prefixPath()))
110
            folder = os.path.join(folder, 'msys')
111
        return folder
112

    
113
    @staticmethod
114
    def grassDescriptionPath():
115
        return os.path.join(os.path.dirname(__file__), 'description')
116

    
117
    @staticmethod
118
    def createGrass7Script(commands):
119
        # Detect cmd encoding
120
        encoding = Grass7Utils.findWindowsCmdEncoding()
121
        folder = Grass7Utils.grassPath()
122
        shell = Grass7Utils.grassWinShell()
123

    
124
        script = Grass7Utils.grassScriptFilename()
125
        gisrc = os.path.join(userFolder(), 'processing.gisrc7')  # FIXME: use temporary file
126
        location = 'temp_location'
127
        gisdbase = Grass7Utils.grassDataFolder()
128

    
129
        # Temporary gisrc file
130
        with open(gisrc, 'w') as output:
131
            command = (u'GISDBASE: {}\n'
132
                       u'LOCATION_NAME: {}\n'
133
                       u'MAPSET: PERMANENT \n'
134
                       u'GRASS_GUI: text\n'
135
                       ).format(gisdbase, location)
136
            output.write(command.encode(encoding))
137

    
138
        with open(script, 'w') as output:
139
            command = (u'set HOME={}\n'
140
                       u'set GISRC={}\n'
141
                       u'set GRASS_SH={}\\bin\\sh.exe\n'
142
                       u'set PATH={};{};%PATH%\n'
143
                       u'set WINGISBASE={}\n'
144
                       u'set GISBASE={}\n'
145
                       u'set GRASS_PROJSHARE={}\n'
146
                       u'set GRASS_MESSAGE_FORMAT=plain\n'
147
                       u'if "%GRASS_ADDON_PATH%"=="" set PATH=%WINGISBASE%\\bin;%WINGISBASE%\\lib;%PATH%\n'
148
                       u'if not "%GRASS_ADDON_PATH%"=="" set PATH=%WINGISBASE%\\bin;%WINGISBASE%\\lib;%GRASS_ADDON_PATH%;%PATH%\n'
149
                       u'\n'
150
                       u'set GRASS_VERSION={}\n'
151
                       u'if not "%LANG%"=="" goto langset\n'
152
                       u'FOR /F "usebackq delims==" %%i IN (`"%WINGISBASE%\\etc\\winlocale"`) DO @set LANG=%%i\n'
153
                       u':langset\n'
154
                       u'\n'
155
                       u'set PATHEXT=%PATHEXT%;.PY\n'
156
                       u'set PYTHONPATH=%PYTHONPATH%;%WINGISBASE%\\etc\\python;%WINGISBASE%\\etc\\wxpython\\n'
157
                       u'\n'
158
                       u'g.gisenv.exe set="MAPSET=PERMANENT"\n'
159
                       u'g.gisenv.exe set="LOCATION={}\n'
160
                       u'g.gisenv.exe set="LOCATION_NAME={}\n'
161
                       u'g.gisenv.exe set="GISDBASE={}\n'
162
                       u'g.gisenv.exe set="GRASS_GUI=text"\n'
163
                       ).format(os.path.expanduser('~').decode('mbcs'), gisrc,
164
                                shell, os.path.join(shell, 'bin'),
165
                                os.path.join(shell, 'lib'), folder, folder,
166
                                os.path.join(folder, 'share', 'proj'),
167
                                Grass7Utils.getGrassVersion(), location,
168
                                location, gisdbase)
169
            output.write(command.encode(encoding))
170
            for command in commands:
171
                output.write(command.encode(encoding) + '\n')
172
            output.write(u'\nexit\n'.encode(encoding))
173

    
174
    @staticmethod
175
    def createGrass7BatchJobFileFromGrass7Commands(commands):
176
        with open(Grass7Utils.grassBatchJobFilename(), 'w') as fout:
177
            for command in commands:
178
                fout.write(command.encode('utf8') + '\n')
179
            fout.write('exit')
180

    
181
    @staticmethod
182
    def grassMapsetFolder():
183
        folder = os.path.join(Grass7Utils.grassDataFolder(), 'temp_location')
184
        mkdir(folder)
185
        return folder
186

    
187
    @staticmethod
188
    def grassDataFolder():
189
        tempfolder = os.path.join(tempFolder(), 'grassdata')
190
        mkdir(tempfolder)
191
        return tempfolder
192

    
193
    @staticmethod
194
    def createTempMapset():
195
        '''Creates a temporary location and mapset(s) for GRASS data
196
        processing. A minimal set of folders and files is created in the
197
        system's default temporary directory. The settings files are
198
        written with sane defaults, so GRASS can do its work. The mapset
199
        projection will be set later, based on the projection of the first
200
        input image or vector
201
        '''
202

    
203
        folder = Grass7Utils.grassMapsetFolder()
204
        mkdir(os.path.join(folder, 'PERMANENT'))
205
        mkdir(os.path.join(folder, 'PERMANENT', '.tmp'))
206
        Grass7Utils.writeGrass7Window(os.path.join(folder, 'PERMANENT', 'DEFAULT_WIND'))
207
        with open(os.path.join(folder, 'PERMANENT', 'MYNAME'), 'w') as outfile:
208
            outfile.write(u'QGIS GRASS GIS 7 interface: temporary data processing location.\n')
209

    
210
        Grass7Utils.writeGrass7Window(os.path.join(folder, 'PERMANENT', 'WIND'))
211
        mkdir(os.path.join(folder, 'PERMANENT', 'sqlite'))
212
        with open(os.path.join(folder, 'PERMANENT', 'VAR'), 'w') as outfile:
213
            outfile.write('DB_DRIVER: sqlite\n')
214
            outfile.write('DB_DATABASE: $GISDBASE/$LOCATION_NAME/$MAPSET/sqlite/sqlite.db\n')
215

    
216
    @staticmethod
217
    def writeGrass7Window(filename):
218
        with open(filename, 'w') as out:
219
            command = (u'proj:       0\n'
220
                       u'zone:       0\n'
221
                       u'north:      1\n'
222
                       u'south:      0\n'
223
                       u'east:       1\n'
224
                       u'west:       0\n'
225
                       u'cols:       1\n'
226
                       u'rows:       1\n'
227
                       u'e-w resol:  1\n'
228
                       u'n-s resol:  1\n'
229
                       u'top:        1\n'
230
                       u'bottom:     0\n'
231
                       u'cols3:      1\n'
232
                       u'rows3:      1\n'
233
                       u'depths:     1\n'
234
                       u'e-w resol3: 1\n'
235
                       u'n-s resol3: 1\n'
236
                       u't-b resol:  1\n')
237
            out.write(command.encode('utf-8'))
238

    
239
    @staticmethod
240
    def prepareGrass7Execution(commands):
241
        env = os.environ.copy()
242

    
243
        if isWindows():
244
            Grass7Utils.createGrass7Script(commands)
245
            command = ['cmd.exe', '/C ', Grass7Utils.grassScriptFilename().encode('mbcs')]
246
        else:
247
            gisrc = os.path.join(userFolder(), 'processing.gisrc7')
248
            env['GISRC'] = gisrc
249
            env['GRASS_MESSAGE_FORMAT'] = 'plain'
250
            env['GRASS_BATCH_JOB'] = Grass7Utils.grassBatchJobFilename()
251
            if 'GISBASE' in env:
252
                del env['GISBASE']
253
            Grass7Utils.createGrass7BatchJobFileFromGrass7Commands(commands)
254
            os.chmod(Grass7Utils.grassBatchJobFilename(), stat.S_IEXEC
255
                     | stat.S_IREAD | stat.S_IWRITE)
256
            if isMac() and os.path.exists(os.path.join(Grass7Utils.grassPath(), 'grass70.sh')):
257
                command = os.path.join(Grass7Utils.grassPath(), 'grass70.sh ', Grass7Utils.grassMapsetFolder(), 'PERMANENT')
258
            else:
259
                command = 'grass70 ' + os.path.join(Grass7Utils.grassMapsetFolder(), 'PERMANENT')
260

    
261
        return command, env
262

    
263
    @staticmethod
264
    def findWindowsCmdEncoding():
265
        """ Find MS-Windows encoding in the shell (cmd.exe)"""
266
        # Creates a temp python file
267
        tempPython = getTempFilenameInTempFolder('cmdEncoding.py')
268
        from qgis.core import QgsMessageLog
269
        QgsMessageLog.logMessage(u'tempPython={}'.format([tempPython.encode('mbcs')]), 'DEBUG', QgsMessageLog.INFO)
270
        with open(tempPython, 'w') as f:
271
            command = (u'# -*- coding: utf-8 -*-\n\n'
272
                       u'import sys\n'
273
                       u'print(sys.stdin.encoding)')
274
            f.write(command.encode('utf-8'))
275

    
276
        env = os.environ.copy()
277
        command = ['cmd.exe', '/C', 'python.exe', tempPython.encode('mbcs')]
278

    
279
        # execute temp python file
280
        p = subprocess.Popen(
281
            command,
282
            shell=True,
283
            stdout=subprocess.PIPE,
284
            stdin=open(os.devnull),
285
            stderr=subprocess.STDOUT,
286
            universal_newlines=True,
287
            env=env)
288
        data = p.communicate()[0]
289

    
290
        # Return codepage
291
        if p.returncode == 0:
292
            return data
293

    
294
        import locale
295
        return locale.getpreferredencoding()
296

    
297
    @staticmethod
298
    def executeGrass7(commands, progress, outputCommands=None):
299
        loglines = []
300
        loglines.append(Grass7Utils.tr('GRASS GIS 7 execution console output'))
301
        grassOutDone = False
302
        command, grassenv = Grass7Utils.prepareGrass7Execution(commands)
303
        proc = subprocess.Popen(
304
            command,
305
            shell=True,
306
            stdout=subprocess.PIPE,
307
            stdin=open(os.devnull),
308
            stderr=subprocess.STDOUT,
309
            universal_newlines=True,
310
            env=grassenv
311
        ).stdout
312
        for line in iter(proc.readline, ''):
313
            if 'GRASS_INFO_PERCENT' in line:
314
                try:
315
                    progress.setPercentage(int(line[len('GRASS_INFO_PERCENT') + 2:]))
316
                except:
317
                    pass
318
            else:
319
                if 'r.out' in line or 'v.out' in line:
320
                    grassOutDone = True
321
                loglines.append(line)
322
                progress.setConsoleInfo(line)
323

    
324
        # Some GRASS scripts, like r.mapcalculator or r.fillnulls, call
325
        # other GRASS scripts during execution. This may override any
326
        # commands that are still to be executed by the subprocess, which
327
        # are usually the output ones. If that is the case runs the output
328
        # commands again.
329

    
330
        if not grassOutDone and outputCommands:
331
            command, grassenv = Grass7Utils.prepareGrass7Execution(outputCommands)
332
            proc = subprocess.Popen(
333
                command,
334
                shell=True,
335
                stdout=subprocess.PIPE,
336
                stdin=open(os.devnull),
337
                stderr=subprocess.STDOUT,
338
                universal_newlines=True,
339
                env=grassenv
340
            ).stdout
341
            for line in iter(proc.readline, ''):
342
                if 'GRASS_INFO_PERCENT' in line:
343
                    try:
344
                        progress.setPercentage(int(
345
                            line[len('GRASS_INFO_PERCENT') + 2:]))
346
                    except:
347
                        pass
348
                else:
349
                    loglines.append(line)
350
                    progress.setConsoleInfo(line)
351

    
352
        if ProcessingConfig.getSetting(Grass7Utils.GRASS_LOG_CONSOLE):
353
            ProcessingLog.addToLog(ProcessingLog.LOG_INFO, loglines)
354

    
355
    # GRASS session is used to hold the layers already exported or
356
    # produced in GRASS between multiple calls to GRASS algorithms.
357
    # This way they don't have to be loaded multiple times and
358
    # following algorithms can use the results of the previous ones.
359
    # Starting a session just involves creating the temp mapset
360
    # structure
361
    @staticmethod
362
    def startGrass7Session():
363
        if not Grass7Utils.sessionRunning:
364
            Grass7Utils.createTempMapset()
365
            Grass7Utils.sessionRunning = True
366

    
367
    # End session by removing the temporary GRASS mapset and all
368
    # the layers.
369
    @staticmethod
370
    def endGrass7Session():
371
        shutil.rmtree(Grass7Utils.grassMapsetFolder(), True)
372
        Grass7Utils.sessionRunning = False
373
        Grass7Utils.sessionLayers = {}
374
        Grass7Utils.projectionSet = False
375

    
376
    @staticmethod
377
    def getSessionLayers():
378
        return Grass7Utils.sessionLayers
379

    
380
    @staticmethod
381
    def addSessionLayers(exportedLayers):
382
        Grass7Utils.sessionLayers = dict(
383
            Grass7Utils.sessionLayers.items()
384
            + exportedLayers.items())
385

    
386
    @staticmethod
387
    def checkGrass7IsInstalled(ignorePreviousState=False):
388
        if isWindows():
389
            path = Grass7Utils.grassPath()
390
            if path == '':
391
                return Grass7Utils.tr(
392
                    'GRASS GIS 7 folder is not configured. Please configure '
393
                    'it before running GRASS GIS 7 algorithms.')
394
            cmdpath = os.path.join(path, 'bin', 'r.out.gdal.exe')
395
            if not os.path.exists(cmdpath):
396
                return Grass7Utils.tr(
397
                    'The specified GRASS 7 folder "{}" does not contain '
398
                    'a valid set of GRASS 7 modules.\nPlease, go to the '
399
                    'Processing settings dialog, and check that the '
400
                    'GRASS 7\nfolder is correctly configured'.format(os.path.join(path, 'bin')))
401

    
402
        if not ignorePreviousState:
403
            if Grass7Utils.isGrass7Installed:
404
                return
405
        try:
406
            from processing import runalg
407
            result = runalg(
408
                'grass7:v.voronoi',
409
                points(),
410
                False,
411
                False,
412
                '270778.60198,270855.745301,4458921.97814,4458983.8488',
413
                -1,
414
                0.0001,
415
                0,
416
                None,
417
            )
418
            if not os.path.exists(result['output']):
419
                return Grass7Utils.tr(
420
                    'It seems that GRASS GIS 7 is not correctly installed and '
421
                    'configured in your system.\nPlease install it before '
422
                    'running GRASS GIS 7 algorithms.')
423
        except:
424
            return Grass7Utils.tr(
425
                'Error while checking GRASS GIS 7 installation. GRASS GIS 7 '
426
                'might not be correctly configured.\n')
427

    
428
        Grass7Utils.isGrass7Installed = True
429

    
430
    @staticmethod
431
    def tr(string, context=''):
432
        if context == '':
433
            context = 'Grass7Utils'
434
        return QCoreApplication.translate(context, string)