diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py index daf80d1f6cde..b42b559075f0 100644 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -167,6 +167,7 @@ from .Ruggedness import Ruggedness from .Hillshade import Hillshade from .ReliefAuto import ReliefAuto +from .Relief import Relief from .IdwInterpolationZValue import IdwInterpolationZValue from .IdwInterpolationAttribute import IdwInterpolationAttribute from .TinInterpolationZValue import TinInterpolationZValue @@ -247,7 +248,7 @@ def __init__(self): OffsetLine(), PolygonCentroids(), Translate(), SingleSidedBuffer(), PointsAlongGeometry(), Aspect(), Slope(), Ruggedness(), Hillshade(), - ReliefAuto(), ZonalStatisticsQgis(), + ReliefAuto(), Relief(), ZonalStatisticsQgis(), IdwInterpolationZValue(), IdwInterpolationAttribute(), TinInterpolationZValue(), TinInterpolationAttribute(), RemoveNullGeometry(), ExtractByExpression(), diff --git a/python/plugins/processing/algs/qgis/Relief.py b/python/plugins/processing/algs/qgis/Relief.py new file mode 100644 index 000000000000..a110d7a9fd40 --- /dev/null +++ b/python/plugins/processing/algs/qgis/Relief.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + Relief.py + --------------------- + Date : December 2016 + Copyright : (C) 2016 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__ = 'December 2016' +__copyright__ = '(C) 2016, Alexander Bruy' + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = '$Format:%H$' + +import os + +from qgis.PyQt.QtGui import QIcon, QColor + +from qgis.analysis import QgsRelief + +from processing.core.GeoAlgorithm import GeoAlgorithm +from processing.core.parameters import (Parameter, + ParameterRaster, + ParameterNumber, + _splitParameterOptions) +from processing.core.outputs import OutputRaster, OutputTable +from processing.tools import raster + +pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] + + +class Relief(GeoAlgorithm): + + INPUT_LAYER = 'INPUT_LAYER' + Z_FACTOR = 'Z_FACTOR' + COLORS = 'COLORS' + OUTPUT_LAYER = 'OUTPUT_LAYER' + FREQUENCY_DISTRIBUTION = 'FREQUENCY_DISTRIBUTION' + + def getIcon(self): + return QIcon(os.path.join(pluginPath, 'images', 'dem.png')) + + def defineCharacteristics(self): + self.name, self.i18n_name = self.trAlgorithm('Relief') + self.group, self.i18n_group = self.trAlgorithm('Raster terrain analysis') + + class ParameterReliefColors(Parameter): + default_metadata = { + 'widget_wrapper': 'processing.algs.qgis.ui.ReliefColorsWidget.ReliefColorsWidgetWrapper' + } + + def __init__(self, name='', description='', parent=None): + Parameter.__init__(self, name, description) + self.parent = parent + + def setValue(self, value): + if value is None: + return False + + if isinstance(value, str): + self.value = value + else: + self.value = ParameterReliefColors.colorsToString(value) + return True + + def getValueAsCommandLineParameter(self): + return '"{}"'.format(self.value) + + def getAsScriptCode(self): + param_type = '' + param_type += 'relief colors ' + return '##' + self.name + '=' + param_type + + @classmethod + def fromScriptCode(self, line): + isOptional, name, definition = _splitParameterOptions(line) + descName = _createDescriptiveName(name) + parent = definition.lower().strip()[len('relief colors') + 1:] + return ParameterReliefColors(name, description, parent) + + @staticmethod + def colorsToString(colors): + s = '' + for c in colors: + s += '{:.2f}, {:.2f}, {:d}, {:d}, {:d};'.format(c[0], + c[1], + c[2], + c[3], + c[4]) + return s[:-1] + + self.addParameter(ParameterRaster(self.INPUT_LAYER, + self.tr('Elevation layer'))) + self.addParameter(ParameterNumber(self.Z_FACTOR, + self.tr('Z factor'), 1.0, 999999.99, 1.0)) + self.addParameter(ParameterReliefColors(self.COLORS, + self.tr('Relief colors'), self.INPUT_LAYER)) + self.addOutput(OutputRaster(self.OUTPUT_LAYER, + self.tr('Relief'))) + self.addOutput(OutputTable(self.FREQUENCY_DISTRIBUTION, + self.tr('Frequency distribution'))) + + def processAlgorithm(self, progress): + inputFile = self.getParameterValue(self.INPUT_LAYER) + zFactor = self.getParameterValue(self.Z_FACTOR) + colors = self.getParameterValue(self.COLORS).split(';') + outputFile = self.getOutputValue(self.OUTPUT_LAYER) + frequencyDistribution = self.getOutputValue(self.FREQUENCY_DISTRIBUTION) + + outputFormat = raster.formatShortNameFromFileName(outputFile) + + reliefColors = [] + for c in colors: + v = c.split(',') + color = QgsRelief.ReliefColor(QColor(int(v[2]), int(v[3]), int(v[4])), + float(v[0]), + float(v[1])) + reliefColors.append(color) + + relief = QgsRelief(inputFile, outputFile, outputFormat) + relief.setReliefColors(reliefColors) + relief.setZFactor(zFactor) + relief.exportFrequencyDistributionToCsv(frequencyDistribution) + relief.processRaster(None) diff --git a/python/plugins/processing/algs/qgis/ui/ReliefColorsWidget.py b/python/plugins/processing/algs/qgis/ui/ReliefColorsWidget.py new file mode 100644 index 000000000000..1004b69ece9c --- /dev/null +++ b/python/plugins/processing/algs/qgis/ui/ReliefColorsWidget.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + ReliefColorsWidget.py + --------------------- + Date : December 2016 + Copyright : (C) 2016 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__ = 'December 2016' +__copyright__ = '(C) 2016, Alexander Bruy' + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = '$Format:%H$' + +import os +import codecs + +from qgis.PyQt import uic +from qgis.PyQt.QtCore import pyqtSlot, QDir +from qgis.PyQt.QtGui import (QIcon, + QBrush, + QColor) +from qgis.PyQt.QtWidgets import (QTreeWidgetItem, + QFileDialog, + QMessageBox, + QInputDialog, + QColorDialog + ) +from qgis.PyQt.QtXml import QDomDocument + +from qgis.core import QgsApplication, QgsMapLayer +from qgis.analysis import QgsRelief + +from processing.gui.wrappers import WidgetWrapper +from processing.tools import system + +pluginPath = os.path.dirname(__file__) +WIDGET, BASE = uic.loadUiType(os.path.join(pluginPath, 'reliefcolorswidgetbase.ui')) + + +class ReliefColorsWidget(BASE, WIDGET): + + def __init__(self): + super(ReliefColorsWidget, self).__init__(None) + self.setupUi(self) + + self.btnAdd.setIcon(QgsApplication.getThemeIcon('/symbologyAdd.svg')) + self.btnRemove.setIcon(QgsApplication.getThemeIcon('/symbologyRemove.svg')) + self.btnUp.setIcon(QgsApplication.getThemeIcon('/symbologyUp.svg')) + self.btnDown.setIcon(QgsApplication.getThemeIcon('/symbologyDown.svg')) + self.btnLoad.setIcon(QgsApplication.getThemeIcon('/mActionFileOpen.svg')) + self.btnSave.setIcon(QgsApplication.getThemeIcon('/mActionFileSave.svg')) + self.btnAuto.setIcon(QgsApplication.getThemeIcon('/mActionDraw.svg')) + + self.layer = None + + @pyqtSlot() + def on_btnAdd_clicked(self): + item = QTreeWidgetItem() + item.setText(0, '0.00') + item.setText(1, '0.00') + item.setBackground(2, QBrush(QColor(127, 127, 127))) + self.reliefClassTree.addTopLevelItem(item) + + @pyqtSlot() + def on_btnRemove_clicked(self): + selectedItems = self.reliefClassTree.selectedItems() + for item in selectedItems: + self.reliefClassTree.invisibleRootItem().removeChild(item) + item = None + + @pyqtSlot() + def on_btnDown_clicked(self): + selectedItems = self.reliefClassTree.selectedItems() + for item in selectedItems: + currentIndex = self.reliefClassTree.indexOfTopLevelItem(item) + if currentIndex < self.reliefClassTree.topLevelItemCount() - 1: + self.reliefClassTree.takeTopLevelItem(currentIndex) + self.reliefClassTree.insertTopLevelItem(currentIndex + 1, item) + self.reliefClassTree.setCurrentItem(item) + + @pyqtSlot() + def on_btnUp_clicked(self): + selectedItems = self.reliefClassTree.selectedItems() + for item in selectedItems: + currentIndex = self.reliefClassTree.indexOfTopLevelItem(item) + if currentIndex > 0: + self.reliefClassTree.takeTopLevelItem(currentIndex) + self.reliefClassTree.insertTopLevelItem(currentIndex - 1, item) + self.reliefClassTree.setCurrentItem(item) + + @pyqtSlot() + def on_btnLoad_clicked(self): + fileName, _ = QFileDialog.getOpenFileName(None, + self.tr('Import Colors and elevations from XML'), + QDir.homePath(), + self.tr('XML files (*.xml *.XML)')) + if fileName == '': + return + + doc = QDomDocument() + with codecs.open(fileName, 'r', encoding='utf-8') as f: + content = f.read() + + if not doc.setContent(content): + QMessageBox.critical(None, + self.tr('Error parsing XML'), + self.tr('The XML file could not be loaded')) + return + + self.reliefClassTree.clear() + reliefColorList = doc.elementsByTagName('ReliefColor') + for i in range(reliefColorList.length()): + elem = reliefColorList.at(i).toElement() + item = QTreeWidgetItem() + item.setText(0, elem.attribute('MinElevation')) + item.setText(1, elem.attribute('MaxElevation')) + item.setBackground(2, QBrush(QColor(int(elem.attribute('red')), + int(elem.attribute('green')), + int(elem.attribute('blue'))))) + self.reliefClassTree.addTopLevelItem(item) + + @pyqtSlot() + def on_btnSave_clicked(self): + fileName, _ = QFileDialog.getSaveFileName(None, + self.tr('Export Colors and elevations as XML'), + QDir.homePath(), + self.tr('XML files (*.xml *.XML)')) + + if fileName == '': + return + + if not fileName.lower().endswith('.xml'): + fileName += '.xml' + + doc = QDomDocument() + colorsElem = doc.createElement('ReliefColors') + doc.appendChild(colorsElem) + + colors = self.reliefColors() + for c in colors: + elem = doc.createElement('ReliefColor') + elem.setAttribute('MinElevation', str(c.minElevation)) + elem.setAttribute('MaxElevation', str(c.maxElevation)) + elem.setAttribute('red', str(c.color.red())) + elem.setAttribute('green', str(c.color.green())) + elem.setAttribute('blue', str(c.color.blue())) + colorsElem.appendChild(elem) + + with codecs.open(fileName, 'w', encoding='utf-8') as f: + f.write(doc.toString(2)) + + @pyqtSlot() + def on_btnAuto_clicked(self): + if self.layer is None: + return + + relief = QgsRelief(self.layer, system.getTempFilename(), 'GTiff') + colors = relief.calculateOptimizedReliefClasses() + self.populateColors(colors) + + @pyqtSlot(QTreeWidgetItem, int) + def on_reliefClassTree_itemDoubleClicked(self, item, column): + if not item: + return + + if column == 0: + d, ok = QInputDialog.getDouble(None, + self.tr('Enter lower elevation class bound'), + self.tr('Elevation'), + float(item.text(0)), + decimals=2) + if ok: + item.setText(0, str(d)) + elif column == 1: + d, ok = QInputDialog.getDouble(None, + self.tr('Enter upper elevation class bound'), + self.tr('Elevation'), + float(item.text(1)), + decimals=2) + if ok: + item.setText(1, str(d)) + elif column == 2: + c = QColorDialog.getColor(item.background(2).color(), + None, + self.tr('Select color for relief class')) + if c.isValid(): + item.setBackground(2, QBrush(c)) + + def reliefColors(self): + colors = [] + for i in range(self.reliefClassTree.topLevelItemCount()): + item = self.reliefClassTree.topLevelItem(i) + if item: + c = QgsRelief.ReliefColor(item.background(2).color(), + float(item.text(0)), + float(item.text(1))) + colors.append(c) + return colors + + def populateColors(self, colors): + self.reliefClassTree.clear() + for c in colors: + item = QTreeWidgetItem() + item.setText(0, str(c.minElevation)) + item.setText(1, str(c.maxElevation)) + item.setBackground(2, QBrush(c.color)) + self.reliefClassTree.addTopLevelItem(item) + + def setLayer(self, layer): + self.layer = layer + + def setValue(self, value): + self.reliefClassTree.clear() + rows = value.split(';') + for r in rows: + v = r.split(',') + item = QTreeWidgetItem() + item.setText(0, v[0]) + item.setText(1, v[1]) + color = QColor(int(v[2]), int(v[3]), int(v[4])) + item.setBackground(2, QBrush(color)) + self.reliefClassTree.addTopLevelItem(item) + + def value(self): + rColors = self.reliefColors() + colors = '' + for c in rColors: + colors += '{:.2f}, {:.2f}, {:d}, {:d}, {:d};'.format(c.minElevation, + c.maxElevation, + c.color.red(), + c.color.green(), + c.color.blue()) + return colors[:-1] + + +class ReliefColorsWidgetWrapper(WidgetWrapper): + + def createWidget(self): + return ReliefColorsWidget() + + def postInitialize(self, wrappers): + for wrapper in wrappers: + if wrapper.param.name == self.param.parent: + self.setLayer(wrapper.value()) + wrapper.widgetValueHasChanged.connect(self.parentValueChanged) + break + + def parentValueChanged(self, wrapper): + self.setLayer(wrapper.value()) + + def setLayer(self, layer): + if isinstance(layer, QgsMapLayer): + layer = layer.source() + self.widget.setLayer(layer) + + def setValue(self, value): + self.widget.setValue(value) + + def value(self): + return self.widget.value() diff --git a/python/plugins/processing/algs/qgis/ui/reliefcolorswidgetbase.ui b/python/plugins/processing/algs/qgis/ui/reliefcolorswidgetbase.ui new file mode 100644 index 000000000000..42c55d365ad2 --- /dev/null +++ b/python/plugins/processing/algs/qgis/ui/reliefcolorswidgetbase.ui @@ -0,0 +1,127 @@ + + + Form + + + + 0 + 0 + 505 + 209 + + + + Form + + + + + + + Lower bound + + + + + Upper bound + + + + + Color + + + + + + + + + + Add row + + + ... + + + + + + + Remove row + + + ... + + + + + + + Move up + + + ... + + + + + + + Move down + + + ... + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Load colors from file + + + ... + + + + + + + Save colors to file + + + ... + + + + + + + Generate color table automatically + + + ... + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 8c8160a8b6e2..7073fd07db61 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -1148,6 +1148,22 @@ tests: hash: 7fe0e0174185fd743e23760f33615adf10f771b4275f320db6f7f4f8 type: rasterhash + - algorithm: qgis:relief + name: Relief (custom colors) + params: + COLORS: 85.00, 104.44, 7, 165, 144;104.44, 104.44, 12, 221, 162;104.44, 104.44, + 33, 252, 183;104.44, 104.44, 247, 252, 152;104.44, 104.44, 252, 196, 8;104.44, + 190.33, 252, 166, 15;190.33, 226.70, 175, 101, 15;226.70, 226.70, 255, 133, + 92;226.70, 243.00, 204, 204, 204 + INPUT_LAYER: + name: dem.tif + type: raster + Z_FACTOR: 1.0 + results: + OUTPUT_LAYER: + hash: 6c79ec9b948c8e878aa490670e8a26a0b6efc5f9d162a0fff1042d80 + type: rasterhash + # Case 1: Keep all fields - algorithm: qgis:lineintersections name: Line Intersection Keep All Fields from Both