From 02fbe42a306a289aa48c6fe39a9c5603d00ac347 Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Mon, 28 Oct 2019 19:42:12 +0200 Subject: [PATCH] [FEATURE][needs-docs][processing] add gdal_viewshed algorithm --- .../algs/gdal/GdalAlgorithmProvider.py | 7 + .../plugins/processing/algs/gdal/viewshed.py | 158 ++++++++++++++++++ .../tests/GdalAlgorithmsRasterTest.py | 58 +++++++ 3 files changed, 223 insertions(+) create mode 100644 python/plugins/processing/algs/gdal/viewshed.py diff --git a/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py b/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py index 37ee6e884c1..988599ff801 100644 --- a/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py +++ b/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py @@ -23,6 +23,8 @@ __copyright__ = '(C) 2012, Victor Olaya' import os +from osgeo import gdal + from qgis.PyQt.QtCore import QCoreApplication from qgis.core import (QgsApplication, QgsProcessingProvider) @@ -68,6 +70,7 @@ from .tri import tri from .warp import warp from .pansharp import pansharp from .rasterize_over_fixed_value import rasterize_over_fixed_value +from .viewshed import viewshed from .extractprojection import ExtractProjection from .rasterize_over import rasterize_over @@ -192,6 +195,10 @@ class GdalAlgorithmProvider(QgsProcessingProvider): PointsAlongLines(), # Ogr2OgrTableToPostGisList(), ] + + if int(gdal.VersionInfo()) > 3010000: + self.algs.append(viewshed()) + for a in self.algs: self.addAlgorithm(a) diff --git a/python/plugins/processing/algs/gdal/viewshed.py b/python/plugins/processing/algs/gdal/viewshed.py new file mode 100644 index 00000000000..59acd2a286d --- /dev/null +++ b/python/plugins/processing/algs/gdal/viewshed.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + viewshed.py + --------------------- + Date : October 2019 + Copyright : (C) 2019 by Alexander Bruy + Email : alexander dot bruy at gmail dot com +*************************************************************************** +* * +* This program is free software; you can redistribute it and/or modify * +* it under the terms of the GNU General Public License as published by * +* the Free Software Foundation; either version 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +__author__ = 'Alexander Bruy' +__date__ = 'October 2019' +__copyright__ = '(C) 2019, Alexander Bruy' + +import os + +from qgis.PyQt.QtGui import QIcon + +from qgis.core import (QgsRasterFileWriter, + QgsProcessingException, + QgsProcessingParameterDefinition, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterBand, + QgsProcessingParameterPoint, + QgsProcessingParameterNumber, + QgsProcessingParameterString, + QgsProcessingParameterRasterDestination) +from processing.algs.gdal.GdalAlgorithm import GdalAlgorithm +from processing.algs.gdal.GdalUtils import GdalUtils + +from processing.tools.system import isWindows + +pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] + + +class viewshed(GdalAlgorithm): + + INPUT = 'INPUT' + BAND = 'BAND' + OBSERVER = 'OBSERVER' + OBSERVER_HEIGHT = 'OBSERVER_HEIGHT' + TARGET_HEIGHT = 'TARGET_HEIGHT' + MAX_DISTANCE = 'MAX_DISTANCE' + OPTIONS = 'OPTIONS' + EXTRA = 'EXTRA' + OUTPUT = 'OUTPUT' + + def __init__(self): + super().__init__() + + def initAlgorithm(self, config=None): + self.addParameter(QgsProcessingParameterRasterLayer(self.INPUT, + self.tr('Input layer'))) + self.addParameter(QgsProcessingParameterBand(self.BAND, + self.tr('Band number'), + 1, + parentLayerParameterName=self.INPUT)) + self.addParameter(QgsProcessingParameterPoint(self.OBSERVER, + self.tr('Observer location'))) + self.addParameter(QgsProcessingParameterNumber(self.OBSERVER_HEIGHT, + self.tr('Observer height'), + type=QgsProcessingParameterNumber.Double, + minValue=0.0, + defaultValue=1.0)) + self.addParameter(QgsProcessingParameterNumber(self.TARGET_HEIGHT, + self.tr('Target height'), + type=QgsProcessingParameterNumber.Double, + minValue=0.0, + defaultValue=1.0)) + self.addParameter(QgsProcessingParameterNumber(self.MAX_DISTANCE, + self.tr('Maximum distance from observer to compute visibility'), + type=QgsProcessingParameterNumber.Double, + minValue=0.0, + defaultValue=100.0)) + + options_param = QgsProcessingParameterString(self.OPTIONS, + self.tr('Additional creation options'), + defaultValue='', + optional=True) + options_param.setFlags(options_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced) + options_param.setMetadata({ + 'widget_wrapper': { + 'class': 'processing.algs.gdal.ui.RasterOptionsWidget.RasterOptionsWidgetWrapper'}}) + self.addParameter(options_param) + + extra_param = QgsProcessingParameterString(self.EXTRA, + self.tr('Additional command-line parameters'), + defaultValue=None, + optional=True) + extra_param.setFlags(extra_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced) + self.addParameter(extra_param) + + self.addParameter(QgsProcessingParameterRasterDestination(self.OUTPUT, + self.tr('Output'))) + + def name(self): + return 'viewshed' + + def displayName(self): + return self.tr('Viewshed') + + def group(self): + return self.tr('Raster miscellaneous') + + def groupId(self): + return 'rastermiscellaneous' + + def commandName(self): + return 'gdal_viewshed' + + def getConsoleCommands(self, parameters, context, feedback, executing=True): + dem = self.parameterAsRasterLayer(parameters, self.INPUT, context) + if dem is None: + raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT)) + + out = self.parameterAsOutputLayer(parameters, self.OUTPUT, context) + self.setOutputValue(self.OUTPUT, out) + + observer = self.parameterAsPoint(parameters, self.OBSERVER, context, dem.crs()) + + arguments = [] + arguments.append('-b') + arguments.append('{}'.format(self.parameterAsInt(parameters, self.BAND, context))) + arguments.append('-ox') + arguments.append('{}'.format(observer.x())) + arguments.append('-oy') + arguments.append('{}'.format(observer.y())) + arguments.append('-oz') + arguments.append('{}'.format(self.parameterAsDouble(parameters, self.OBSERVER_HEIGHT, context))) + arguments.append('-tz') + arguments.append('{}'.format(self.parameterAsDouble(parameters, self.TARGET_HEIGHT, context))) + arguments.append('-md') + arguments.append('{}'.format(self.parameterAsDouble(parameters, self.MAX_DISTANCE, context))) + + arguments.append('-f') + arguments.append(QgsRasterFileWriter.driverForExtension(os.path.splitext(out)[1])) + + options = self.parameterAsString(parameters, self.OPTIONS, context) + if options: + arguments.extend(GdalUtils.parseCreationOptions(options)) + + if self.EXTRA in parameters and parameters[self.EXTRA] not in (None, ''): + extra = self.parameterAsString(parameters, self.EXTRA, context) + arguments.append(extra) + + arguments.append(dem.source()) + arguments.append(out) + + return [self.commandName(), GdalUtils.escapeAndJoin(arguments)] diff --git a/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py b/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py index 30eb51a05fc..157db806dda 100644 --- a/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py +++ b/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py @@ -69,6 +69,8 @@ from processing.algs.gdal.nearblack import nearblack from processing.algs.gdal.slope import slope from processing.algs.gdal.rasterize_over import rasterize_over from processing.algs.gdal.rasterize_over_fixed_value import rasterize_over_fixed_value +from processing.algs.gdal.viewshed import viewshed + testDataPath = os.path.join(os.path.dirname(__file__), 'testdata') @@ -2344,6 +2346,62 @@ class TestGdalRasterAlgorithms(unittest.TestCase, AlgorithmsTestBase.AlgorithmsT '-r cubic -of GTiff -bitdepth 12 -threads ALL_CPUS' ]) + def testGdalViewshed(self): + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + dem = os.path.join(testDataPath, 'dem.tif') + + with tempfile.TemporaryDirectory() as outdir: + outsource = outdir + '/out.tif' + alg = viewshed() + alg.initAlgorithm() + + # defaults + self.assertEqual( + alg.getConsoleCommands({'INPUT': dem, + 'BAND': 1, + 'OBSERVER': '18.67274,45.80599', + 'OUTPUT': outsource}, context, feedback), + ['gdal_viewshed', + '-b 1 -ox 18.67274 -oy 45.80599 -oz 1.0 -tz 1.0 -md 100.0 -f GTiff ' + + dem + ' ' + outsource + ]) + + self.assertEqual( + alg.getConsoleCommands({'INPUT': dem, + 'BAND': 2, + 'OBSERVER': '18.67274,45.80599', + 'OBSERVER_HEIGHT': 1.8, + 'TARGET_HEIGHT': 20, + 'MAX_DISTANCE': 1000, + 'OUTPUT': outsource}, context, feedback), + ['gdal_viewshed', + '-b 2 -ox 18.67274 -oy 45.80599 -oz 1.8 -tz 20.0 -md 1000.0 -f GTiff ' + + dem + ' ' + outsource + ]) + + self.assertEqual( + alg.getConsoleCommands({'INPUT': dem, + 'BAND': 1, + 'OBSERVER': '18.67274,45.80599', + 'EXTRA': '-a_nodata=-9999 -cc 0.2', + 'OUTPUT': outsource}, context, feedback), + ['gdal_viewshed', + '-b 1 -ox 18.67274 -oy 45.80599 -oz 1.0 -tz 1.0 -md 100.0 -f GTiff ' + + '-a_nodata=-9999 -cc 0.2 ' + dem + ' ' + outsource + ]) + + self.assertEqual( + alg.getConsoleCommands({'INPUT': dem, + 'BAND': 1, + 'OBSERVER': '18.67274,45.80599', + 'OPTIONS': 'COMPRESS=DEFLATE|PREDICTOR=2|ZLEVEL=9', + 'OUTPUT': outsource}, context, feedback), + ['gdal_viewshed', + '-b 1 -ox 18.67274 -oy 45.80599 -oz 1.0 -tz 1.0 -md 100.0 -f GTiff ' + + '-co COMPRESS=DEFLATE -co PREDICTOR=2 -co ZLEVEL=9 ' + dem + ' ' + outsource + ]) + def testBuildVrt(self): context = QgsProcessingContext() feedback = QgsProcessingFeedback()