""" *************************************************************************** InterpolationDataWidget.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" import os from typing import Optional, Union from qgis.PyQt import uic from qgis.PyQt.QtCore import pyqtSignal from qgis.PyQt.QtWidgets import QTreeWidgetItem, QComboBox from qgis.core import ( Qgis, QgsApplication, QgsWkbTypes, QgsRectangle, QgsReferencedRectangle, QgsCoordinateReferenceSystem, QgsProcessingUtils, QgsProcessingParameterNumber, QgsProcessingParameterDefinition, QgsFieldProxyModel, QgsVectorLayer, ) from qgis.gui import QgsDoubleSpinBox from qgis.analysis import QgsInterpolator from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD from processing.tools import dataobjects pluginPath = os.path.dirname(__file__) class ParameterInterpolationData(QgsProcessingParameterDefinition): def __init__(self, name="", description=""): super().__init__(name, description) self.setMetadata( { "widget_wrapper": "processing.algs.qgis.ui.InterpolationWidgets.InterpolationDataWidgetWrapper" } ) def type(self): return "idw_interpolation_data" def clone(self): return ParameterInterpolationData(self.name(), self.description()) @staticmethod def parseValue(value): if value is None: return None if value == "": return None if isinstance(value, str): return value if value != "" else None else: return ParameterInterpolationData.dataToString(value) @staticmethod def dataToString(data): s = "" for c in data: s += f"{c[0]}::~::{c[1]}::~::{c[2]:d}::~::{c[3]:d};" return s[:-1] WIDGET, BASE = uic.loadUiType( os.path.join(pluginPath, "interpolationdatawidgetbase.ui") ) class InterpolationDataWidget(BASE, WIDGET): hasChanged = pyqtSignal() def __init__(self): super().__init__(None) self.setupUi(self) self.btnAdd.setIcon(QgsApplication.getThemeIcon("/symbologyAdd.svg")) self.btnRemove.setIcon(QgsApplication.getThemeIcon("/symbologyRemove.svg")) self.btnAdd.clicked.connect(self.addLayer) self.btnRemove.clicked.connect(self.removeLayer) self.cmbLayers.layerChanged.connect(self.layerChanged) self.cmbLayers.setFilters(Qgis.LayerFilter.VectorLayer) self.cmbFields.setFilters(QgsFieldProxyModel.Filter.Numeric) self.layerChanged(self.cmbLayers.currentLayer()) def addLayer(self): layer = self.cmbLayers.currentLayer() attribute = "" if self.chkUseZCoordinate.isChecked(): attribute = "Z_COORD" else: attribute = self.cmbFields.currentField() self._addLayerData(layer.name(), attribute) self.hasChanged.emit() def removeLayer(self): item = self.layersTree.currentItem() if not item: return self.layersTree.invisibleRootItem().removeChild(item) self.hasChanged.emit() def layerChanged(self, layer: Optional[QgsVectorLayer]): self.chkUseZCoordinate.setEnabled(False) self.chkUseZCoordinate.setChecked(False) if layer is None or not layer.isValid(): return provider = layer.dataProvider() if not provider: return if QgsWkbTypes.hasZ(provider.wkbType()): self.chkUseZCoordinate.setEnabled(True) self.cmbFields.setLayer(layer) def _addLayerData(self, layerName: str, attribute: str): item = QTreeWidgetItem() item.setText(0, layerName) item.setText(1, attribute) self.layersTree.addTopLevelItem(item) comboBox = QComboBox() comboBox.addItem(self.tr("Points")) comboBox.addItem(self.tr("Structure lines")) comboBox.addItem(self.tr("Break lines")) comboBox.setCurrentIndex(0) self.layersTree.setItemWidget(item, 2, comboBox) def setValue(self, value: str): self.layersTree.clear() rows = value.split("::|::") for i, r in enumerate(rows): v = r.split("::~::") layer = QgsProcessingUtils.mapLayerFromString( v[0], dataobjects.createContext() ) if layer is None or not layer.isValid(): continue field_index = int(v[2]) if field_index == -1: field_name = "Z_COORD" else: field_name = layer.fields().at(field_index).name() self._addLayerData(v[0], field_name) comboBox = self.layersTree.itemWidget(self.layersTree.topLevelItem(i), 2) comboBox.setCurrentIndex(int(v[3])) self.hasChanged.emit() def value(self) -> str: layers = "" context = dataobjects.createContext() for i in range(self.layersTree.topLevelItemCount()): item = self.layersTree.topLevelItem(i) if item: layerName = item.text(0) layer = QgsProcessingUtils.mapLayerFromString(layerName, context) if not layer: continue provider = layer.dataProvider() if not provider: continue interpolationAttribute = item.text(1) interpolationSource = QgsInterpolator.ValueSource.Attribute if interpolationAttribute == "Z_COORD": interpolationSource = QgsInterpolator.ValueSource.Z fieldIndex = -1 else: fieldIndex = layer.fields().indexFromName(interpolationAttribute) comboBox = self.layersTree.itemWidget( self.layersTree.topLevelItem(i), 2 ) inputTypeName = comboBox.currentText() if inputTypeName == self.tr("Points"): inputType = QgsInterpolator.SourceType.Points elif inputTypeName == self.tr("Structure lines"): inputType = QgsInterpolator.SourceType.StructureLines else: inputType = QgsInterpolator.SourceType.BreakLines layers += "{}::~::{:d}::~::{:d}::~::{:d}::|::".format( layer.source(), interpolationSource, fieldIndex, inputType ) return layers[: -len("::|::")] class InterpolationDataWidgetWrapper(WidgetWrapper): def createWidget(self): widget = InterpolationDataWidget() widget.hasChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) return widget def setValue(self, value): self.widget.setValue(value) def value(self): return self.widget.value() class ParameterPixelSize(QgsProcessingParameterNumber): def __init__( self, name="", description="", layersData=None, extent=None, minValue=None, default=None, optional=False, ): QgsProcessingParameterNumber.__init__( self, name, description, QgsProcessingParameterNumber.Type.Double, default, optional, minValue, ) self.setMetadata( { "widget_wrapper": "processing.algs.qgis.ui.InterpolationWidgets.PixelSizeWidgetWrapper" } ) self.layersData = layersData self.extent = extent self.layers = [] def clone(self): copy = ParameterPixelSize( self.name(), self.description(), self.layersData, self.extent, self.minimum(), self.defaultValue(), self.flags() & QgsProcessingParameterDefinition.Flag.FlagOptional, ) return copy WIDGET, BASE = uic.loadUiType(os.path.join(pluginPath, "RasterResolutionWidget.ui")) class PixelSizeWidget(BASE, WIDGET): def __init__(self): super().__init__(None) self.setupUi(self) self.context = dataobjects.createContext() self.extent: Optional[Union[QgsReferencedRectangle, QgsRectangle]] = ( QgsRectangle() ) self.layers = [] self.mCellXSpinBox.setShowClearButton(False) self.mCellYSpinBox.setShowClearButton(False) self.mRowsSpinBox.setShowClearButton(False) self.mColumnsSpinBox.setShowClearButton(False) self.mCellYSpinBox.valueChanged.connect(self.mCellXSpinBox.setValue) self.mCellXSpinBox.valueChanged.connect(self.pixelSizeChanged) self.mRowsSpinBox.valueChanged.connect(self.rowsChanged) self.mColumnsSpinBox.valueChanged.connect(self.columnsChanged) def setLayers(self, layersData: str): self.extent = QgsRectangle() self.layers = [] for row in layersData.split(";"): v = row.split("::~::") # need to keep a reference until interpolation is complete layer = QgsProcessingUtils.variantToSource(v[0], self.context) if layer: self.layers.append(layer) self.extent.combineExtentWith(layer.sourceExtent()) self.pixelSizeChanged() def setExtent(self, extent: Optional[str]): if extent is not None: tokens = extent.split(" ")[0].split(",") ext = QgsRectangle( float(tokens[0]), float(tokens[2]), float(tokens[1]), float(tokens[3]) ) if len(tokens) > 1: self.extent = QgsReferencedRectangle( ext, QgsCoordinateReferenceSystem(tokens[1][1:-1]) ) else: self.extent = ext self.pixelSizeChanged() def pixelSizeChanged(self): cell_size = self.mCellXSpinBox.value() if cell_size <= 0 or self.extent.isNull(): return self.mCellYSpinBox.blockSignals(True) self.mCellYSpinBox.setValue(cell_size) self.mCellYSpinBox.blockSignals(False) rows = max(round(self.extent.height() / cell_size) + 1, 1) cols = max(round(self.extent.width() / cell_size) + 1, 1) self.mRowsSpinBox.blockSignals(True) self.mRowsSpinBox.setValue(rows) self.mRowsSpinBox.blockSignals(False) self.mColumnsSpinBox.blockSignals(True) self.mColumnsSpinBox.setValue(cols) self.mColumnsSpinBox.blockSignals(False) def rowsChanged(self): rows = self.mRowsSpinBox.value() if rows <= 0 or self.extent.isNull(): return cell_size = self.extent.height() / rows if cell_size == 0: return cols = max(round(self.extent.width() / cell_size) + 1, 1) self.mColumnsSpinBox.blockSignals(True) self.mColumnsSpinBox.setValue(cols) self.mColumnsSpinBox.blockSignals(False) for w in [self.mCellXSpinBox, self.mCellYSpinBox]: w.blockSignals(True) w.setValue(cell_size) w.blockSignals(False) def columnsChanged(self): cols = self.mColumnsSpinBox.value() if cols < 2 or self.extent.isNull(): return cell_size = self.extent.width() / (cols - 1) if cell_size == 0: return rows = max(round(self.extent.height() / cell_size), 1) self.mRowsSpinBox.blockSignals(True) self.mRowsSpinBox.setValue(rows) self.mRowsSpinBox.blockSignals(False) for w in [self.mCellXSpinBox, self.mCellYSpinBox]: w.blockSignals(True) w.setValue(cell_size) w.blockSignals(False) def setValue(self, value): try: numeric_value = float(value) except: return False self.mCellXSpinBox.setValue(numeric_value) self.mCellYSpinBox.setValue(numeric_value) return True def value(self): return self.mCellXSpinBox.value() class PixelSizeWidgetWrapper(WidgetWrapper): def __init__(self, param, dialog, row=0, col=0, **kwargs): super().__init__(param, dialog, row, col, **kwargs) self.context = dataobjects.createContext() def _panel(self): return PixelSizeWidget() def createWidget(self): if self.dialogType == DIALOG_STANDARD: return self._panel() else: w = QgsDoubleSpinBox() w.setShowClearButton(False) w.setMinimum(0) w.setMaximum(99999999999) w.setDecimals(6) w.setToolTip( self.tr("Resolution of each pixel in output raster, in layer units") ) return w def postInitialize(self, wrappers): if self.dialogType != DIALOG_STANDARD: return for wrapper in wrappers: if ( wrapper.parameterDefinition().name() == self.parameterDefinition().layersData ): self.setLayers(wrapper.parameterValue()) wrapper.widgetValueHasChanged.connect(self.layersChanged) elif ( wrapper.parameterDefinition().name() == self.parameterDefinition().extent ): self.setExtent(wrapper.parameterValue()) wrapper.widgetValueHasChanged.connect(self.extentChanged) def layersChanged(self, wrapper): self.setLayers(wrapper.parameterValue()) def setLayers(self, layersData: str): self.widget.setLayers(layersData) def extentChanged(self, wrapper): self.setExtent(wrapper.parameterValue()) def setExtent(self, extent: Optional[str]): self.widget.setExtent(extent) def setValue(self, value): return self.widget.setValue(value) def value(self): return self.widget.value()