SagaAlgorithm.py

Pedro Venâncio, 2013-10-16 04:42 AM

Download (23.2 KB)

 
1
# -*- coding: utf-8 -*-
2

    
3
"""
4
***************************************************************************
5
    SagaAlgorithm.py
6
    ---------------------
7
    Date                 : August 2012
8
    Copyright            : (C) 2012 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__ = 'August 2012'
22
__copyright__ = '(C) 2012, 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 os
29
import importlib
30

    
31
from qgis.core import *
32
from PyQt4.QtCore import *
33
from PyQt4.QtGui import *
34

    
35
from processing.core.GeoAlgorithm import GeoAlgorithm
36
from processing.core.ProcessingConfig import ProcessingConfig
37
from processing.core.ProcessingLog import ProcessingLog
38
from processing.core.GeoAlgorithmExecutionException import \
39
        GeoAlgorithmExecutionException
40
from processing.parameters.ParameterTable import ParameterTable
41
from processing.parameters.ParameterMultipleInput import ParameterMultipleInput
42
from processing.parameters.ParameterRaster import ParameterRaster
43
from processing.parameters.ParameterNumber import ParameterNumber
44
from processing.parameters.ParameterSelection import ParameterSelection
45
from processing.parameters.ParameterTableField import ParameterTableField
46
from processing.parameters.ParameterExtent import ParameterExtent
47
from processing.parameters.ParameterFixedTable import ParameterFixedTable
48
from processing.parameters.ParameterVector import ParameterVector
49
from processing.parameters.ParameterBoolean import ParameterBoolean
50
from processing.parameters.ParameterFactory import ParameterFactory
51
from processing.outputs.OutputFactory import OutputFactory
52
from processing.outputs.OutputTable import OutputTable
53
from processing.outputs.OutputVector import OutputVector
54
from processing.outputs.OutputRaster import OutputRaster
55
from processing.saga.SagaUtils import SagaUtils
56
from processing.saga.SagaGroupNameDecorator import SagaGroupNameDecorator
57
from processing.tools import dataobjects
58
from processing.tools.system import *
59

    
60
sessionExportedLayers = {}
61

    
62

    
63
class SagaAlgorithm(GeoAlgorithm):
64

    
65
    OUTPUT_EXTENT = 'OUTPUT_EXTENT'
66

    
67
    def __init__(self, descriptionfile):
68
        # True if it should resample
69
        self.resample = True
70

    
71
        # In case several non-matching raster layers are used as input
72
        GeoAlgorithm.__init__(self)
73
        self.descriptionFile = descriptionfile
74
        self.defineCharacteristicsFromFile()
75
        if self.resample:
76
            # Reconsider resampling policy now that we know the input
77
            # parameters
78
            self.resample = self.setResamplingPolicy()
79

    
80
    def getCopy(self):
81
        newone = SagaAlgorithm(self.descriptionFile)
82
        newone.provider = self.provider
83
        return newone
84

    
85
    def setResamplingPolicy(self):
86
        count = 0
87
        for param in self.parameters:
88
            if isinstance(param, ParameterRaster):
89
                count += 1
90
            if isinstance(param, ParameterMultipleInput):
91
                if param.datatype == ParameterMultipleInput.TYPE_RASTER:
92
                    return True
93

    
94
        return count > 1
95

    
96
    def getIcon(self):
97
        return QIcon(os.path.dirname(__file__) + '/../images/saga.png')
98

    
99
    def defineCharacteristicsFromFile(self):
100
        self.hardcodedStrings = []
101
        lines = open(self.descriptionFile)
102
        line = lines.readline().strip('\n').strip()
103
        self.name = line
104
        if '|' in self.name:
105
            tokens = self.name.split('|')
106
            self.name = tokens[0]
107
            self.cmdname = tokens[1]
108
        else:
109
            self.cmdname = self.name
110
            self.name = self.name[0].upper() + self.name[1:].lower()
111
        line = lines.readline().strip('\n').strip()
112
        self.undecoratedGroup = line
113
        self.group = SagaGroupNameDecorator.getDecoratedName(
114
                self.undecoratedGroup)
115
        while line != '':
116
            line = line.strip('\n').strip()
117
            if line.startswith('Hardcoded'):
118
                self.hardcodedStrings.append(line[len('Harcoded|') + 1:])
119
            elif line.startswith('Parameter'):
120
                self.addParameter(ParameterFactory.getFromString(line))
121
            elif line.startswith('DontResample'):
122
                self.resample = False
123
            elif line.startswith('Extent'):
124
                # An extent parameter that wraps 4 SAGA numerical parameters
125
                self.extentParamNames = line[6:].strip().split(' ')
126
                self.addParameter(ParameterExtent(self.OUTPUT_EXTENT,
127
                                  'Output extent', '0,1,0,1'))
128
            else:
129
                self.addOutput(OutputFactory.getFromString(line))
130
            line = lines.readline().strip('\n').strip()
131
        lines.close()
132

    
133
    def calculateResamplingExtent(self):
134
        """This method calculates the resampling extent, but it might
135
        set self.resample to False if, with the current layers, there
136
        is no need to resample.
137
        """
138

    
139
        auto = ProcessingConfig.getSetting(SagaUtils.SAGA_AUTO_RESAMPLING)
140
        if auto:
141
            first = True
142
            self.inputExtentsCount = 0
143
            for param in self.parameters:
144
                if param.value:
145
                    if isinstance(param, ParameterRaster):
146
                        if isinstance(param.value, QgsRasterLayer):
147
                            layer = param.value
148
                        else:
149
                            layer = dataobjects.getObjectFromUri(param.value)
150
                        self.addToResamplingExtent(layer, first)
151
                        first = False
152
                    if isinstance(param, ParameterMultipleInput):
153
                        if param.datatype \
154
                            == ParameterMultipleInput.TYPE_RASTER:
155
                            layers = param.value.split(';')
156
                            for layername in layers:
157
                                layer = dataobjects.getObjectFromUri(layername)
158
                                self.addToResamplingExtent(layer, first)
159
                                first = False
160
            if self.inputExtentsCount < 2:
161
                self.resample = False
162
        else:
163
            self.xmin = ProcessingConfig.getSetting(
164
                    SagaUtils.SAGA_RESAMPLING_REGION_XMIN)
165
            self.xmax = ProcessingConfig.getSetting(
166
                    SagaUtils.SAGA_RESAMPLING_REGION_XMAX)
167
            self.ymin = ProcessingConfig.getSetting(
168
                    SagaUtils.SAGA_RESAMPLING_REGION_YMIN)
169
            self.ymax = ProcessingConfig.getSetting(
170
                    SagaUtils.SAGA_RESAMPLING_REGION_YMAX)
171
            self.cellsize = ProcessingConfig.getSetting(
172
                    SagaUtils.SAGA_RESAMPLING_REGION_CELLSIZE)
173

    
174
    def addToResamplingExtent(self, layer, first):
175
        if layer is None:
176
            return
177
        if first:
178
            self.inputExtentsCount = 1
179
            self.xmin = layer.extent().xMinimum()
180
            self.xmax = layer.extent().xMaximum()
181
            self.ymin = layer.extent().yMinimum()
182
            self.ymax = layer.extent().yMaximum()
183
            self.cellsize = (layer.extent().xMaximum()
184
                             - layer.extent().xMinimum()) / layer.width()
185
        else:
186
            cellsize = (layer.extent().xMaximum() -
187
                        layer.extent().xMinimum()) / layer.width()
188
            if self.xmin != layer.extent().xMinimum() or self.xmax \
189
                != layer.extent().xMaximum() or self.ymin \
190
                != layer.extent().yMinimum() or self.ymax \
191
                != layer.extent().yMaximum() or self.cellsize != cellsize:
192
                self.xmin = min(self.xmin, layer.extent().xMinimum())
193
                self.xmax = max(self.xmax, layer.extent().xMaximum())
194
                self.ymin = min(self.ymin, layer.extent().yMinimum())
195
                self.ymax = max(self.ymax, layer.extent().yMaximum())
196
                self.cellsize = max(self.cellsize, cellsize)
197
                self.inputExtentsCount += 1
198

    
199
    def processAlgorithm(self, progress):
200
        if isWindows():
201
            path = SagaUtils.sagaPath()
202
            if path == '':
203
                raise GeoAlgorithmExecutionException(
204
                        'SAGA folder is not configured.\nPlease configure \
205
                        it before running SAGA algorithms.')
206
        commands = list()
207
        self.exportedLayers = {}
208

    
209
        self.preProcessInputs()
210

    
211
        # 1: Export rasters to sgrd and vectors to shp
212
        # Tables must be in dbf format. We check that.
213
        if self.resample:
214
            self.calculateResamplingExtent()
215
        for param in self.parameters:
216
            if isinstance(param, ParameterRaster):
217
                if param.value is None:
218
                    continue
219
                value = param.value
220
                if not value.endswith('sgrd'):
221
                    exportCommand = self.exportRasterLayer(value)
222
                    if exportCommand is not None:
223
                        commands.append(exportCommand)
224
                if self.resample:
225
                    commands.append(self.resampleRasterLayer(value))
226
            if isinstance(param, ParameterVector):
227
                if param.value is None:
228
                    continue
229
                layer = dataobjects.getObjectFromUri(param.value, False)
230
                if layer:
231
                    filename = dataobjects.exportVectorLayer(layer)
232
                    self.exportedLayers[param.value] = filename
233
                elif not param.value.endswith('shp'):
234
                    raise GeoAlgorithmExecutionException(
235
                            'Unsupported file format')
236
            if isinstance(param, ParameterTable):
237
                if param.value is None:
238
                    continue
239
                table = dataobjects.getObjectFromUri(param.value, False)
240
                if table:
241
                    filename = dataobjects.exportTable(table)
242
                    self.exportedLayers[param.value] = filename
243
                elif not param.value.endswith('shp'):
244
                    raise GeoAlgorithmExecutionException(
245
                            'Unsupported file format')
246
            if isinstance(param, ParameterMultipleInput):
247
                if param.value is None:
248
                    continue
249
                layers = param.value.split(';')
250
                if layers is None or len(layers) == 0:
251
                    continue
252
                if param.datatype == ParameterMultipleInput.TYPE_RASTER:
253
                    for layerfile in layers:
254
                        if not layerfile.endswith('sgrd'):
255
                            exportCommand = self.exportRasterLayer(layerfile)
256
                            if exportCommand is not None:
257
                                commands.append(exportCommand)
258
                        if self.resample:
259
                            commands.append(
260
                                    self.resampleRasterLayer(layerfile))
261
                elif param.datatype == ParameterMultipleInput.TYPE_VECTOR_ANY:
262
                    for layerfile in layers:
263
                        layer = dataobjects.getObjectFromUri(layerfile, False)
264
                        if layer:
265
                            filename = dataobjects.exportVectorLayer(layer)
266
                            self.exportedLayers[layerfile] = filename
267
                        elif not layerfile.endswith('shp'):
268
                            raise GeoAlgorithmExecutionException(
269
                                    'Unsupported file format')
270

    
271
        # 2: Set parameters and outputs
272
        saga208 = ProcessingConfig.getSetting(SagaUtils.SAGA_208)
273
        if isWindows() or isMac() or not saga208:
274
            command = self.undecoratedGroup + ' "' + self.cmdname + '"'
275
        else:
276
            command = 'lib' + self.undecoratedGroup + ' "' + self.cmdname + '"'
277

    
278
        if self.hardcodedStrings:
279
            for s in self.hardcodedStrings:
280
                command += ' ' + s
281

    
282
        for param in self.parameters:
283
            if param.value is None:
284
                continue
285
            if isinstance(param, (ParameterRaster, ParameterVector,
286
                          ParameterTable)):
287
                value = param.value
288
                if value in self.exportedLayers.keys():
289
                    command += ' -' + param.name + ' "' \
290
                        + self.exportedLayers[value] + '"'
291
                else:
292
                    command += ' -' + param.name + ' "' + value + '"'
293
            elif isinstance(param, ParameterMultipleInput):
294
                s = param.value
295
                for layer in self.exportedLayers.keys():
296
                    s = s.replace(layer, self.exportedLayers[layer])
297
                command += ' -' + param.name + ' "' + s + '"'
298
            elif isinstance(param, ParameterBoolean):
299
                if param.value:
300
                    command += ' -' + param.name
301
            elif isinstance(param, ParameterFixedTable):
302
                tempTableFile = getTempFilename('txt')
303
                f = open(tempTableFile, 'w')
304
                f.write('\t'.join([col for col in param.cols]) + '\n')
305
                values = param.value.split(',')
306
                for i in range(0, len(values), 3):
307
                    s = values[i] + '\t' + values[i + 1] + '\t' + values[i
308
                            + 2] + '\n'
309
                    f.write(s)
310
                f.close()
311
                command += ' -' + param.name + ' "' + tempTableFile + '"'
312
            elif isinstance(param, ParameterExtent):
313
                # 'We have to substract/add half cell size, since SAGA is
314
                # center based, not corner based
315
                halfcell = self.getOutputCellsize() / 2
316
                offset = [halfcell, -halfcell, halfcell, -halfcell]
317
                values = param.value.split(',')
318
                for i in range(4):
319
                    command += ' -' + self.extentParamNames[i] + ' ' \
320
                        + str(float(values[i]) + offset[i])
321
            elif isinstance(param, (ParameterNumber, ParameterSelection)):
322
                command += ' -' + param.name + ' ' + str(param.value)
323
            else:
324
                command += ' -' + param.name + ' "' + str(param.value) + '"'
325

    
326
        for out in self.outputs:
327
            if isinstance(out, OutputRaster):
328
                filename = out.getCompatibleFileName(self)
329
                filename += '.sgrd'
330
                command += ' -' + out.name + ' "' + filename + '"'
331
            if isinstance(out, OutputVector):
332
                filename = out.getCompatibleFileName(self)
333
                command += ' -' + out.name + ' "' + filename + '"'
334
            if isinstance(out, OutputTable):
335
                filename = out.getCompatibleFileName(self)
336
                command += ' -' + out.name + ' "' + filename + '"'
337

    
338
        commands.append(command)
339

    
340
        # 3: Export resulting raster layers
341
        optim = ProcessingConfig.getSetting(
342
                SagaUtils.SAGA_IMPORT_EXPORT_OPTIMIZATION)
343
        for out in self.outputs:
344
            if isinstance(out, OutputRaster):
345
                filename = out.getCompatibleFileName(self)
346
                filename2 = filename + '.sgrd'
347
                formatIndex = (4 if not saga208 and isWindows() else 1)
348
                sessionExportedLayers[filename] = filename2
349
                dontExport = True
350

    
351
                # Do not export is the output is not a final output
352
                # of the model
353
                if self.model is not None and optim:
354
                    for subalg in self.model.algOutputs:
355
                        if out.name in subalg:
356
                            if subalg[out.name] is not None:
357
                                dontExport = False
358
                                break
359
                    if dontExport:
360
                        continue
361

    
362
                if self.cmdname == 'RGB Composite':
363
                        if isWindows() or isMac() or not saga208:
364
                                   commands.append('io_grid_image 0 -IS_RGB -GRID:"' + filename2
365
                                        + '" -FILE:"' + filename
366
                                        + '"')
367
                        else:
368
                                   commands.append('libio_grid_image 0 -IS_RGB -GRID:"' + filename2
369
                                        + '" -FILE:"' + filename
370
                                        + '"')
371
                else:
372
                        if isWindows() or isMac() or not saga208:
373
                            commands.append('io_gdal 1 -GRIDS "' + filename2
374
                                            + '" -FORMAT ' + str(formatIndex)
375
                                            + ' -TYPE 0 -FILE "' + filename + '"')
376
                        else:
377
                            commands.append('libio_gdal 1 -GRIDS "' + filename2
378
                                            + '" -FORMAT 1 -TYPE 0 -FILE "' + filename
379
                                            + '"')
380

    
381
        # 4: Run SAGA
382
        commands = self.editCommands(commands)
383
        SagaUtils.createSagaBatchJobFileFromSagaCommands(commands)
384
        loglines = []
385
        loglines.append('SAGA execution commands')
386
        for line in commands:
387
            progress.setCommand(line)
388
            loglines.append(line)
389
        if ProcessingConfig.getSetting(SagaUtils.SAGA_LOG_COMMANDS):
390
            ProcessingLog.addToLog(ProcessingLog.LOG_INFO, loglines)
391
        SagaUtils.executeSaga(progress)
392

    
393
    def preProcessInputs(self):
394
        name = self.commandLineName().replace('.', '_')[len('saga:'):]
395
        try:
396
            module = importlib.import_module('processing.saga.ext.' + name)
397
        except ImportError:
398
            return
399
        if hasattr(module, 'preProcessInputs'):
400
            func = getattr(module, 'preProcessInputs')
401
            func(self)
402

    
403
    def editCommands(self, commands):
404
        name = self.commandLineName()[len('saga:'):]
405
        try:
406
            module = importlib.import_module('processing.saga.ext.' + name)
407
        except ImportError:
408
            return commands
409
        if hasattr(module, 'editCommands'):
410
            func = getattr(module, 'editCommands')
411
            return func(commands)
412
        else:
413
            return commands
414

    
415
    def getOutputCellsize(self):
416
        """Tries to guess the cellsize of the output, searching for
417
        a parameter with an appropriate name for it.
418
        """
419

    
420
        cellsize = 0
421
        for param in self.parameters:
422
            if param.value is not None and param.name == 'USER_SIZE':
423
                cellsize = float(param.value)
424
                break
425
        return cellsize
426

    
427
    def resampleRasterLayer(self, layer):
428
        """This is supposed to be run after having exported all raster
429
        layers.
430
        """
431

    
432
        if layer in self.exportedLayers.keys():
433
            inputFilename = self.exportedLayers[layer]
434
        else:
435
            inputFilename = layer
436
        destFilename = getTempFilename('sgrd')
437
        self.exportedLayers[layer] = destFilename
438
        saga208 = ProcessingConfig.getSetting(SagaUtils.SAGA_208)
439
        if isWindows() or isMac() or not saga208:
440
            s = 'grid_tools "Resampling" -INPUT "' + inputFilename \
441
                + '" -TARGET 0 -SCALE_UP_METHOD 4 -SCALE_DOWN_METHOD 4 -USER_XMIN ' \
442
                + str(self.xmin) + ' -USER_XMAX ' + str(self.xmax) \
443
                + ' -USER_YMIN ' + str(self.ymin) + ' -USER_YMAX ' \
444
                + str(self.ymax) + ' -USER_SIZE ' + str(self.cellsize) \
445
                + ' -USER_GRID "' + destFilename + '"'
446
        else:
447
            s = 'libgrid_tools "Resampling" -INPUT "' + inputFilename \
448
                + '" -TARGET 0 -SCALE_UP_METHOD 4 -SCALE_DOWN_METHOD 4 -USER_XMIN ' \
449
                + str(self.xmin) + ' -USER_XMAX ' + str(self.xmax) \
450
                + ' -USER_YMIN ' + str(self.ymin) + ' -USER_YMAX ' \
451
                + str(self.ymax) + ' -USER_SIZE ' + str(self.cellsize) \
452
                + ' -USER_GRID "' + destFilename + '"'
453
        return s
454

    
455
    def exportRasterLayer(self, source):
456
        if source in sessionExportedLayers:
457
            self.exportedLayers[source] = sessionExportedLayers[source]
458
            return None
459
        layer = dataobjects.getObjectFromUri(source, False)
460
        if layer:
461
            filename = str(layer.name())
462
        else:
463
            filename = os.path.basename(source)
464
        validChars = \
465
            'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:'
466
        filename = ''.join(c for c in filename if c in validChars)
467
        if len(filename) == 0:
468
            filename = 'layer'
469
        destFilename = getTempFilenameInTempFolder(filename + '.sgrd')
470
        self.exportedLayers[source] = destFilename
471
        sessionExportedLayers[source] = destFilename
472
        saga208 = ProcessingConfig.getSetting(SagaUtils.SAGA_208)
473
        if isWindows() or isMac() or not saga208:
474
            return 'io_gdal 0 -TRANSFORM -INTERPOL 0 -GRIDS "' + destFilename + '" -FILES "' + source \
475
                + '"'
476
        else:
477
            return 'libio_gdal 0 -GRIDS "' + destFilename + '" -FILES "' \
478
                + source + '"'
479

    
480
    def checkBeforeOpeningParametersDialog(self):
481
        msg = SagaUtils.checkSagaIsInstalled()
482
        if msg is not None:
483
            html = '<p>This algorithm requires SAGA to be run.Unfortunately, \
484
                   it seems that SAGA is not installed in your system, or it \
485
                   is not correctly configured to be used from QGIS</p>'
486
            html += '<p><a href= "http://docs.qgis.org/2.0/html/en/docs/user_manual/processing/3rdParty.html">Click here</a> to know more about how to install and configure SAGA to be used with QGIS</p>'
487
            return html
488

    
489
    def checkParameterValuesBeforeExecuting(self):
490
        """We check that there are no multiband layers, which are not
491
        supported by SAGA.
492
        """
493

    
494
        for param in self.parameters:
495
            if isinstance(param, ParameterRaster):
496
                value = param.value
497
                layer = dataobjects.getObjectFromUri(value)
498
                if layer is not None and layer.bandCount() > 1:
499
                    return 'Input layer ' + str(layer.name()) \
500
                        + ' has more than one band.\n' \
501
                        + 'Multiband layers are not supported by SAGA'
502

    
503
    def helpFile(self):
504
        return os.path.join(os.path.dirname(__file__), 'help',
505
                            self.name.replace(' ', '') + '.html')
506

    
507
    def getPostProcessingErrorMessage(self, wrongLayers):
508
        html = GeoAlgorithm.getPostProcessingErrorMessage(self, wrongLayers)
509
        msg = SagaUtils.checkSagaIsInstalled(True)
510
        html += '<p>This algorithm requires SAGA to be run. A test to check \
511
                 if SAGA is correctly installed and configured in your system \
512
                 has been performed, with the following result:</p><ul><i>'
513
        if msg is None:
514
            html += 'SAGA seems to be correctly installed and \
515
                     configured</li></ul>'
516
        else:
517
            html += msg + '</i></li></ul>'
518
            html += '<p><a href= "http://docs.qgis.org/2.0/html/en/docs/user_manual/processing/3rdParty.html">Click here</a> to know more about how to install and configure SAGA to be used with QGIS</p>'
519

    
520
        return html