Grass7Utils.py

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

Download (17 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
        with open(tempPython, 'w') as f:
269
            command = (u'# -*- coding: utf-8 -*-\n\n'
270
                       u'import sys\n'
271
                       u'print(sys.stdin.encoding)')
272
            f.write(command.encode('utf-8'))
273

    
274
        env = os.environ.copy()
275
        command = ['cmd.exe', '/C', 'python.exe', tempPython]
276

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

    
288
        # Return codepage
289
        if p.returncode == 0:
290
            return data
291

    
292
        import locale
293
        return locale.getpreferredencoding()
294

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

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

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

    
350
        if ProcessingConfig.getSetting(Grass7Utils.GRASS_LOG_CONSOLE):
351
            ProcessingLog.addToLog(ProcessingLog.LOG_INFO, loglines)
352

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

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

    
374
    @staticmethod
375
    def getSessionLayers():
376
        return Grass7Utils.sessionLayers
377

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

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

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

    
426
        Grass7Utils.isGrass7Installed = True
427

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