diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index 71791026acc..e03acd2d54d 100644 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -114,9 +114,6 @@ qgis:extractbyexpression: > For more information about expressions see the user manual -qgis:fieldcalculator: > - This algorithm computes a new vector layer with the same features of the input layer, but with an additional attribute. The values of this new attribute are computed from each feature using a mathematical formula, based on the properties and attributes of the feature. - qgis:findprojection: > This algorithm allows creation of a shortlist of possible candidate coordinate reference systems for a layer with an unknown projection. diff --git a/python/plugins/processing/algs/qgis/FieldsCalculator.py b/python/plugins/processing/algs/qgis/FieldsCalculator.py deleted file mode 100644 index 07ecc1c81d0..00000000000 --- a/python/plugins/processing/algs/qgis/FieldsCalculator.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - FieldsCalculator.py - --------------------- - Date : August 2012 - Copyright : (C) 2012 by Victor Olaya - Email : volayaf 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__ = 'Victor Olaya' -__date__ = 'August 2012' -__copyright__ = '(C) 2012, Victor Olaya' - -from qgis.PyQt.QtCore import QVariant -from qgis.core import (QgsExpression, - QgsExpressionContext, - QgsExpressionContextUtils, - QgsFeatureSink, - QgsField, - QgsDistanceArea, - QgsProcessing, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterEnum, - QgsProcessingParameterNumber, - QgsProcessingParameterBoolean, - QgsProcessingParameterExpression, - QgsProcessingParameterString, - QgsProcessingParameterFeatureSink, - QgsProcessingException) -from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm - -from .ui.FieldsCalculatorDialog import FieldsCalculatorDialog - - -class FieldsCalculator(QgisAlgorithm): - INPUT = 'INPUT' - NEW_FIELD = 'NEW_FIELD' - FIELD_NAME = 'FIELD_NAME' - FIELD_TYPE = 'FIELD_TYPE' - FIELD_LENGTH = 'FIELD_LENGTH' - FIELD_PRECISION = 'FIELD_PRECISION' - FORMULA = 'FORMULA' - OUTPUT = 'OUTPUT' - - TYPES = [QVariant.Double, QVariant.Int, QVariant.String, QVariant.Date] - - def group(self): - return self.tr('Vector table') - - def groupId(self): - return 'vectortable' - - def __init__(self): - super().__init__() - self.type_names = [self.tr('Float'), - self.tr('Integer'), - self.tr('String'), - self.tr('Date')] - - def initAlgorithm(self, config=None): - self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, self.tr('Input layer'), - types=[QgsProcessing.TypeVector])) - self.addParameter(QgsProcessingParameterString(self.FIELD_NAME, - self.tr('Result field name'))) - self.addParameter(QgsProcessingParameterEnum(self.FIELD_TYPE, - self.tr('Field type'), options=self.type_names)) - self.addParameter(QgsProcessingParameterNumber(self.FIELD_LENGTH, - self.tr('Field length'), minValue=0, defaultValue=10)) - self.addParameter(QgsProcessingParameterNumber(self.FIELD_PRECISION, - self.tr('Field precision'), minValue=0, maxValue=15, defaultValue=3)) - self.addParameter(QgsProcessingParameterBoolean(self.NEW_FIELD, - self.tr('Create new field'), defaultValue=True)) - self.addParameter(QgsProcessingParameterExpression(self.FORMULA, self.tr('Formula'))) - self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, - self.tr('Calculated'))) - - def name(self): - return 'fieldcalculator' - - def displayName(self): - return self.tr('Field calculator') - - def processAlgorithm(self, parameters, context, feedback): - source = self.parameterAsSource(parameters, self.INPUT, context) - if source is None: - raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) - - layer = self.parameterAsVectorLayer(parameters, self.INPUT, context) - field_name = self.parameterAsString(parameters, self.FIELD_NAME, context) - field_type = self.TYPES[self.parameterAsEnum(parameters, self.FIELD_TYPE, context)] - width = self.parameterAsInt(parameters, self.FIELD_LENGTH, context) - precision = self.parameterAsInt(parameters, self.FIELD_PRECISION, context) - new_field = self.parameterAsBoolean(parameters, self.NEW_FIELD, context) - formula = self.parameterAsString(parameters, self.FORMULA, context) - - expression = QgsExpression(formula) - da = QgsDistanceArea() - da.setSourceCrs(source.sourceCrs(), context.transformContext()) - da.setEllipsoid(context.ellipsoid()) - expression.setGeomCalculator(da) - - expression.setDistanceUnits(context.distanceUnit()) - expression.setAreaUnits(context.areaUnit()) - - fields = source.fields() - field_index = fields.lookupField(field_name) - if new_field or field_index < 0: - fields.append(QgsField(field_name, field_type, '', width, precision)) - - (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, - fields, source.wkbType(), source.sourceCrs()) - if sink is None: - raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) - - exp_context = self.createExpressionContext(parameters, context) - if layer is not None: - exp_context.appendScope(QgsExpressionContextUtils.layerScope(layer)) - - expression.prepare(exp_context) - - features = source.getFeatures() - total = 100.0 / source.featureCount() if source.featureCount() else 0 - - for current, f in enumerate(features): - if feedback.isCanceled(): - break - - rownum = current + 1 - exp_context.setFeature(f) - exp_context.lastScope().setVariable("row_number", rownum) - value = expression.evaluate(exp_context) - if expression.hasEvalError(): - feedback.reportError(expression.evalErrorString()) - else: - attrs = f.attributes() - if new_field or field_index < 0: - attrs.append(value) - else: - attrs[field_index] = value - f.setAttributes(attrs) - sink.addFeature(f, QgsFeatureSink.FastInsert) - feedback.setProgress(int(current * total)) - - return {self.OUTPUT: dest_id} - - def checkParameterValues(self, parameters, context): - newField = self.parameterAsBoolean(parameters, self.NEW_FIELD, context) - fieldName = self.parameterAsString(parameters, self.FIELD_NAME, context).strip() - if newField and len(fieldName) == 0: - return False, self.tr('Field name is not set. Please enter a field name') - return super(FieldsCalculator, self).checkParameterValues(parameters, context) - - def createCustomParametersWidget(self, parent): - return FieldsCalculatorDialog(self) diff --git a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py index 1d201c9fc16..1022b624886 100644 --- a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py @@ -42,7 +42,6 @@ from .EliminateSelection import EliminateSelection from .ExecuteSQL import ExecuteSQL from .ExportGeometryInfo import ExportGeometryInfo from .FieldPyculator import FieldsPyculator -from .FieldsCalculator import FieldsCalculator from .FindProjection import FindProjection from .GeometryConvert import GeometryConvert from .Heatmap import Heatmap @@ -112,7 +111,6 @@ class QgisAlgorithmProvider(QgsProcessingProvider): EliminateSelection(), ExecuteSQL(), ExportGeometryInfo(), - FieldsCalculator(), FieldsPyculator(), FindProjection(), GeometryConvert(), diff --git a/python/plugins/processing/algs/qgis/ui/DlgFieldsCalculator.ui b/python/plugins/processing/algs/qgis/ui/DlgFieldsCalculator.ui deleted file mode 100644 index 2184d3038e3..00000000000 --- a/python/plugins/processing/algs/qgis/ui/DlgFieldsCalculator.ui +++ /dev/null @@ -1,276 +0,0 @@ - - - FieldsCalculator - - - - 0 - 0 - 681 - 681 - - - - Field calculator - - - - - - - 0 - 0 - - - - Create a new field - - - true - - - true - - - true - - - - QLayout::SetMinimumSize - - - 3 - - - 3 - - - 3 - - - 0 - - - 3 - - - - - Output field name - - - mOutputFieldNameLineEdit - - - - - - - - - - Output field type - - - mOutputFieldTypeComboBox - - - - - - - - - - Output field width - - - mOutputFieldWidthSpinBox - - - - - - - Width of complete output. For example 123,456 means 6 as field width. - - - 0 - - - 15 - - - - - - - Precision - - - mOutputFieldPrecisionSpinBox - - - - - - - 2 - - - - - - - - - - - 3 - 0 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - Input layer - - - - - - - - 0 - 0 - - - - - - - - - - Update existing field - - - true - - - true - - - false - - - - - - - - - - - - false - - - - - - - - - Output file - - - - - - - - - - - - - - - - - - - 0 - - - - - - - - QgsExpressionBuilderWidget - QWidget -
qgis.gui
- 1 -
- - QgsMapLayerComboBox - QComboBox -
qgis.gui
- 0 -
-
- - mOutputFieldNameLineEdit - mOutputFieldTypeComboBox - mOutputFieldWidthSpinBox - mOutputFieldPrecisionSpinBox - mButtonBox - - - - - mButtonBox - accepted() - FieldsCalculator - accept() - - - 679 - 559 - - - 157 - 274 - - - - - mButtonBox - rejected() - FieldsCalculator - reject() - - - 679 - 559 - - - 286 - 274 - - - - -
diff --git a/python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py b/python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py deleted file mode 100644 index b14b53553e6..00000000000 --- a/python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py +++ /dev/null @@ -1,270 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - FieldsCalculatorDialog.py - --------------------- - Date : October 2013 - Copyright : (C) 2013 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 2013' -__copyright__ = '(C) 2013, Alexander Bruy' - -import os -import re -import warnings - -from qgis.PyQt import uic -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtWidgets import QDialog, QFileDialog, QApplication, QMessageBox -from qgis.PyQt.QtGui import QCursor -from qgis.core import (Qgis, - QgsExpressionContextUtils, - QgsProcessingFeedback, - QgsSettings, - QgsMapLayerProxyModel, - QgsProperty, - QgsProject, - QgsMessageLog, - QgsMapLayerType, - QgsProcessingOutputLayerDefinition) -from qgis.gui import QgsEncodingFileDialog, QgsGui -from qgis.utils import OverrideCursor, iface - -from processing.core.ProcessingConfig import ProcessingConfig -from processing.core.ProcessingLog import ProcessingLog -from processing.gui.AlgorithmExecutor import execute -from processing.tools import dataobjects -from processing.gui.Postprocessing import handleAlgorithmResults - -pluginPath = os.path.dirname(__file__) -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - WIDGET, BASE = uic.loadUiType( - os.path.join(pluginPath, 'DlgFieldsCalculator.ui')) - - -class FieldCalculatorFeedback(QgsProcessingFeedback): - """ - Directs algorithm feedback to an algorithm dialog - """ - - def __init__(self, dialog): - QgsProcessingFeedback.__init__(self) - self.dialog = dialog - - def reportError(self, msg, fatalError=False): - self.dialog.error(msg) - - -class FieldsCalculatorDialog(BASE, WIDGET): - - def __init__(self, alg): - super(FieldsCalculatorDialog, self).__init__(None) - self.setupUi(self) - - self.executed = False - self._wasExecuted = False - self.alg = alg - self.layer = None - - self.cmbInputLayer.setFilters(QgsMapLayerProxyModel.VectorLayer) - try: - if iface.activeLayer().type() == QgsMapLayerType.VectorLayer: - self.cmbInputLayer.setLayer(iface.activeLayer()) - except: - pass - - self.cmbInputLayer.layerChanged.connect(self.updateLayer) - self.btnBrowse.clicked.connect(self.selectFile) - self.mNewFieldGroupBox.toggled.connect(self.toggleExistingGroup) - self.mUpdateExistingGroupBox.toggled.connect(self.toggleNewGroup) - self.mOutputFieldTypeComboBox.currentIndexChanged.connect(self.setupSpinboxes) - - # Default values for field width and precision - self.mOutputFieldWidthSpinBox.setValue(10) - self.mOutputFieldPrecisionSpinBox.setValue(3) - - # Output is a shapefile, so limit maximum field name length - self.mOutputFieldNameLineEdit.setMaxLength(10) - - self.manageGui() - - def manageGui(self): - if hasattr(self.leOutputFile, 'setPlaceholderText'): - self.leOutputFile.setPlaceholderText( - self.tr('[Save to temporary file]')) - - self.mOutputFieldTypeComboBox.blockSignals(True) - for t in self.alg.type_names: - self.mOutputFieldTypeComboBox.addItem(t) - self.mOutputFieldTypeComboBox.blockSignals(False) - self.builder.loadRecent('fieldcalc') - - self.updateLayer(self.cmbInputLayer.currentLayer()) - - def initContext(self): - exp_context = self.builder.expressionContext() - exp_context.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(self.layer)) - exp_context.lastScope().setVariable("row_number", 1) - exp_context.setHighlightedVariables(["row_number"]) - self.builder.setExpressionContext(exp_context) - - def updateLayer(self, layer): - self.layer = layer - self.builder.setLayer(self.layer) - self.initContext() - self.populateFields() - - def setupSpinboxes(self, index): - if index != 0: - self.mOutputFieldPrecisionSpinBox.setEnabled(False) - else: - self.mOutputFieldPrecisionSpinBox.setEnabled(True) - - if index == 0: - self.mOutputFieldWidthSpinBox.setRange(1, 20) - self.mOutputFieldWidthSpinBox.setValue(10) - self.mOutputFieldPrecisionSpinBox.setRange(0, 15) - self.mOutputFieldPrecisionSpinBox.setValue(3) - elif index == 1: - self.mOutputFieldWidthSpinBox.setRange(1, 10) - self.mOutputFieldWidthSpinBox.setValue(10) - elif index == 2: - self.mOutputFieldWidthSpinBox.setRange(1, 255) - self.mOutputFieldWidthSpinBox.setValue(80) - else: - self.mOutputFieldWidthSpinBox.setEnabled(False) - self.mOutputFieldPrecisionSpinBox.setEnabled(False) - - def selectFile(self): - output = self.alg.parameterDefinition('OUTPUT') - fileFilter = output.createFileFilter() - - settings = QgsSettings() - if settings.contains('/Processing/LastOutputPath'): - path = settings.value('/Processing/LastOutputPath') - else: - path = ProcessingConfig.getSetting(ProcessingConfig.OUTPUT_FOLDER) - lastEncoding = settings.value('/Processing/encoding', 'System') - fileDialog = QgsEncodingFileDialog(self, - self.tr('Save file'), - path, - fileFilter, - lastEncoding) - fileDialog.setFileMode(QFileDialog.AnyFile) - fileDialog.setAcceptMode(QFileDialog.AcceptSave) - fileDialog.setOption(QFileDialog.DontConfirmOverwrite, False) - if fileDialog.exec_() == QDialog.Accepted: - files = fileDialog.selectedFiles() - encoding = str(fileDialog.encoding()) - output.encoding = encoding - filename = str(files[0]) - selectedFileFilter = str(fileDialog.selectedNameFilter()) - if not filename.lower().endswith( - tuple(re.findall("\\*(\\.[a-z]{1,10})", fileFilter))): - ext = re.search("\\*(\\.[a-z]{1,10})", selectedFileFilter) - if ext: - filename = filename + ext.group(1) - self.leOutputFile.setText(filename) - settings.setValue('/Processing/LastOutputPath', - os.path.dirname(filename)) - settings.setValue('/Processing/encoding', encoding) - - def toggleExistingGroup(self, toggled): - self.mUpdateExistingGroupBox.setChecked(not toggled) - - def toggleNewGroup(self, toggled): - self.mNewFieldGroupBox.setChecked(not toggled) - - def populateFields(self): - if self.layer is None: - return - - self.mExistingFieldComboBox.clear() - fields = self.layer.fields() - for f in fields: - self.mExistingFieldComboBox.addItem(f.name()) - - def getParamValues(self): - if self.mUpdateExistingGroupBox.isChecked(): - fieldName = self.mExistingFieldComboBox.currentText() - else: - fieldName = self.mOutputFieldNameLineEdit.text() - - layer = self.cmbInputLayer.currentLayer() - - context = dataobjects.createContext() - - parameters = {} - parameters['INPUT'] = layer - parameters['FIELD_NAME'] = fieldName - parameters['FIELD_TYPE'] = self.mOutputFieldTypeComboBox.currentIndex() - parameters['FIELD_LENGTH'] = self.mOutputFieldWidthSpinBox.value() - parameters['FIELD_PRECISION'] = self.mOutputFieldPrecisionSpinBox.value() - parameters['NEW_FIELD'] = self.mNewFieldGroupBox.isChecked() - parameters['FORMULA'] = self.builder.expressionText() - output = QgsProcessingOutputLayerDefinition() - if self.leOutputFile.text().strip(): - output.sink = QgsProperty.fromValue(self.leOutputFile.text().strip()) - else: - output.sink = QgsProperty.fromValue('memory:') - output.destinationProject = context.project() - parameters['OUTPUT'] = output - - ok, msg = self.alg.checkParameterValues(parameters, context) - if not ok: - QMessageBox.warning( - self, self.tr('Unable to execute algorithm'), msg) - return {} - return parameters - - def accept(self): - keepOpen = ProcessingConfig.getSetting(ProcessingConfig.KEEP_DIALOG_OPEN) - parameters = self.getParamValues() - if parameters: - with OverrideCursor(Qt.WaitCursor): - self.feedback = FieldCalculatorFeedback(self) - self.feedback.progressChanged.connect(self.setPercentage) - - context = dataobjects.createContext() - ProcessingLog.addToLog(self.alg.asPythonCommand(parameters, context)) - QgsGui.instance().processingRecentAlgorithmLog().push(self.alg.id()) - - self.executed, results = execute(self.alg, parameters, context, self.feedback) - self.setPercentage(0) - - if self.executed: - handleAlgorithmResults(self.alg, - context, - self.feedback, - not keepOpen, - parameters) - self._wasExecuted = self.executed or self._wasExecuted - if not keepOpen: - QDialog.reject(self) - - def reject(self): - self.executed = False - QDialog.reject(self) - - def setPercentage(self, i): - self.progressBar.setValue(i) - - def error(self, text): - QMessageBox.critical(self, "Error", text) - QgsMessageLog.logMessage(text, self.tr('Processing'), Qgis.Critical) - - def wasExecuted(self): - return self._wasExecuted diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml index 39dfb342cb3..34424de16a2 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml @@ -915,6 +915,22 @@ tests: name: expected/field_calculator_points.gml type: vector + - algorithm: native:fieldcalculator + name: Test field calculator points (new API) + params: + FIELD_LENGTH: 10 + FIELD_PRECISION: 3 + FIELD_TYPE: 0 + FORMULA: "\"id2\" *2" + INPUT: + name: points.gml + type: vector + NEW_FIELD_NAME: test + results: + OUTPUT: + name: expected/field_calculator_points.gml + type: vector + - algorithm: qgis:advancedpythonfieldcalculator name: Test advanced python calculator params: diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 52f78ce0641..e3f43007a9c 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -72,6 +72,7 @@ SET(QGIS_ANALYSIS_SRCS processing/qgsalgorithmextractzmvalues.cpp processing/qgsalgorithmextractvertices.cpp processing/qgsalgorithmextractspecificvertices.cpp + processing/qgsalgorithmfieldcalculator.cpp processing/qgsalgorithmfiledownloader.cpp processing/qgsalgorithmfillnodata.cpp processing/qgsalgorithmfilter.cpp diff --git a/src/analysis/processing/qgsalgorithmfieldcalculator.cpp b/src/analysis/processing/qgsalgorithmfieldcalculator.cpp new file mode 100644 index 00000000000..2eba43fc58b --- /dev/null +++ b/src/analysis/processing/qgsalgorithmfieldcalculator.cpp @@ -0,0 +1,225 @@ +/*************************************************************************** + qgsalgorithmfieldcalculator.h + ---------------------- + begin : September 2020 + copyright : (C) 2020 by Ivan Ivanov + email : ivan@opengis.ch + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + + +#include "qgsalgorithmfieldcalculator.h" +#include "qgsexpressioncontextutils.h" + +///@cond PRIVATE + +QString QgsFieldCalculatorAlgorithm::name() const +{ + return QStringLiteral( "fieldcalculator" ); +} + +QString QgsFieldCalculatorAlgorithm::displayName() const +{ + return QObject::tr( "Field calculator" ); +} + +QStringList QgsFieldCalculatorAlgorithm::tags() const +{ + return QObject::tr( "field,calculator,vector" ).split( ',' ); +} + +QString QgsFieldCalculatorAlgorithm::group() const +{ + return QObject::tr( "Vector table" ); +} + +QString QgsFieldCalculatorAlgorithm::groupId() const +{ + return QStringLiteral( "vectortable" ); +} + +QString QgsFieldCalculatorAlgorithm::outputName() const +{ + return QObject::tr( "Calculated" ); +} + +QList QgsFieldCalculatorAlgorithm::inputLayerTypes() const +{ + return QList() << QgsProcessing::TypeVector; +} + +QgsProcessingFeatureSource::Flag QgsFieldCalculatorAlgorithm::sourceFlags() const +{ + return QgsProcessingFeatureSource::FlagSkipGeometryValidityChecks; +} + +void QgsFieldCalculatorAlgorithm::initParameters( const QVariantMap &configuration ) +{ + Q_UNUSED( configuration ); + + QStringList fieldTypes = QStringList( {QObject::tr( "Float" ), QObject::tr( "Integer" ), QObject::tr( "String" ), QObject::tr( "Date" ) } ); + + std::unique_ptr< QgsProcessingParameterField > existingFieldName = qgis::make_unique< QgsProcessingParameterField > ( QStringLiteral( "EXISTING_FIELD_NAME" ), QObject::tr( "Result in existing field" ), QVariant(), QStringLiteral( "INPUT" ), QgsProcessingParameterField::Any, false, true ); + std::unique_ptr< QgsProcessingParameterString > newFieldName = qgis::make_unique< QgsProcessingParameterString > ( QStringLiteral( "NEW_FIELD_NAME" ), QObject::tr( "Result in new field" ), QVariant(), false, true ); + std::unique_ptr< QgsProcessingParameterEnum > fieldType = qgis::make_unique< QgsProcessingParameterEnum > ( QStringLiteral( "FIELD_TYPE" ), QObject::tr( "Result field type" ), fieldTypes, false, 0 ); + std::unique_ptr< QgsProcessingParameterNumber > fieldLength = qgis::make_unique< QgsProcessingParameterNumber > ( QStringLiteral( "FIELD_LENGTH" ), QObject::tr( "Result field length" ), QgsProcessingParameterNumber::Integer, QVariant( 0 ), false, 0 ); + std::unique_ptr< QgsProcessingParameterNumber > fieldPrecision = qgis::make_unique< QgsProcessingParameterNumber > ( QStringLiteral( "FIELD_PRECISION" ), QObject::tr( "Result field precision" ), QgsProcessingParameterNumber::Integer, QVariant( 0 ), false, 0 ); + std::unique_ptr< QgsProcessingParameterExpression > expression = qgis::make_unique< QgsProcessingParameterExpression> ( QStringLiteral( "FORMULA" ), QObject::tr( "Formula" ), QVariant(), QStringLiteral( "INPUT" ), false ); + + expression->setMetadata( QVariantMap( {{"inlineEditor", true}} ) ); + + addParameter( existingFieldName.release() ); + addParameter( newFieldName.release() ); + addParameter( fieldType.release() ); + addParameter( fieldLength.release() ); + addParameter( fieldPrecision.release() ); + addParameter( expression.release() ); +} + +QgsFields QgsFieldCalculatorAlgorithm::outputFields( const QgsFields & ) const +{ + return mFields; +} + +QString QgsFieldCalculatorAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm computes a new vector layer with the same features of the input layer, " + "but either overwriting an existing attribute or adding an additional attribute. The values of this field" + "are computed from each feature using an expression, based on the properties and attributes of the feature." + "Note that selecting a value in \"Result in existing field\" will ignore all the rest of the " + "field settings." ); +} + +QgsFieldCalculatorAlgorithm *QgsFieldCalculatorAlgorithm::createInstance() const +{ + return new QgsFieldCalculatorAlgorithm(); +} + + +bool QgsFieldCalculatorAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * ) +{ + std::unique_ptr< QgsProcessingFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + + if ( !source ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + QList fieldTypes( {QVariant::Double, QVariant::Int, QVariant::String, QVariant::Date} ); + + // prepare fields + const int fieldTypeIdx = parameterAsInt( parameters, QStringLiteral( "FIELD_TYPE" ), context ); + const int fieldLength = parameterAsInt( parameters, QStringLiteral( "FIELD_LENGTH" ), context ); + const int fieldPrecision = parameterAsInt( parameters, QStringLiteral( "FIELD_PRECISION" ), context ); + const QString existingFieldName = parameterAsString( parameters, QStringLiteral( "EXISTING_FIELD_NAME" ), context ); + const QString newFieldName = parameterAsString( parameters, QStringLiteral( "NEW_FIELD_NAME" ), context ); + + QVariant::Type fieldType = fieldTypes[fieldTypeIdx]; + + // this is to keep backwards compatibility, "NEW_FIELD" flags what how "FIELD_NAME" should be treated + // since they are not defined parameters, they should be accessed directly from `parameters` + bool isNewField = parameters.value( QStringLiteral( "NEW_FIELD" ) ).toBool(); + QString fieldName = parameters.value( QStringLiteral( "FIELD_NAME" ) ).toString(); + + // In a perfect universe there would be only "EXISTING_FIELD_NAME" and "NEW_FIELD_NAME" + if ( !parameters.contains( QStringLiteral( "NEW_FIELD" ) ) ) + { + isNewField = existingFieldName.isEmpty(); + fieldName = isNewField ? newFieldName : existingFieldName; + } + + if ( fieldName.isEmpty() ) + throw QgsProcessingException( QObject::tr( "Field name must not be an empty string" ) ); + + const QgsField field( + fieldName, + fieldType, + QString(), + fieldLength, + fieldPrecision + ); + + mFields = source->fields(); + + int fieldIdx = mFields.lookupField( field.name() ); + + if ( isNewField || fieldIdx < 0 ) + mFields.append( field ); + + QString dest; + + mFieldIdx = mFields.lookupField( field.name() ); + + // prepare expression + QString expressionString = parameterAsString( parameters, QStringLiteral( "FORMULA" ), context ); + mExpressionContext = createExpressionContext( parameters, context, source.get() ); + mExpression = QgsExpression( expressionString ); + mDa.setSourceCrs( source->sourceCrs(), context.transformContext() ); + mDa.setEllipsoid( context.ellipsoid() ); + + mExpression.setGeomCalculator( &mDa ); + mExpression.setDistanceUnits( context.distanceUnit() ); + mExpression.setAreaUnits( context.areaUnit() ); + + if ( mExpression.hasParserError() ) + throw QgsProcessingException( QObject::tr( "Parser error with formula expression \"%2\": %3" ) + .arg( expressionString, mExpression.parserErrorString() ) ); + + mExpression.prepare( &mExpressionContext ); + + return true; +} + +QgsFeatureList QgsFieldCalculatorAlgorithm::processFeature( const QgsFeature &feature, QgsProcessingContext &, QgsProcessingFeedback * ) +{ + QgsAttributes attributes( mFields.size() ); + const QStringList fieldNames = mFields.names(); + for ( const QString &fieldName : fieldNames ) + { + const int attributeIndex = feature.fieldNameIndex( fieldName ); + + if ( attributeIndex >= 0 ) + attributes[attributeIndex] = feature.attribute( fieldName ); + } + + if ( mExpression.isValid() ) + { + mExpressionContext.setFeature( feature ); + mExpressionContext.lastScope()->setVariable( QStringLiteral( "row_number" ), mRowNumber ); + + const QVariant value = mExpression.evaluate( &mExpressionContext ); + + if ( mExpression.hasEvalError() ) + { + throw QgsProcessingException( QObject::tr( "Evaluation error in expression \"%1\": %2" ) + .arg( mExpression.expression(), mExpression.evalErrorString() ) ); + } + + attributes[mFieldIdx] = value; + attributes.append( value ); + } + else + { + attributes.append( QVariant() ); + } + + QgsFeature f = feature; + f.setAttributes( attributes ); + mRowNumber++; + return QgsFeatureList() << f; +} + +bool QgsFieldCalculatorAlgorithm::supportInPlaceEdit( const QgsMapLayer *layer ) const +{ + Q_UNUSED( layer ) + return false; +} + +///@endcond + diff --git a/src/analysis/processing/qgsalgorithmfieldcalculator.h b/src/analysis/processing/qgsalgorithmfieldcalculator.h new file mode 100644 index 00000000000..e85478ca9af --- /dev/null +++ b/src/analysis/processing/qgsalgorithmfieldcalculator.h @@ -0,0 +1,68 @@ +/*************************************************************************** + qgsalgorithmfieldcalculator.h + ---------------------- + begin : September 2020 + copyright : (C) 2020 by Ivan Ivanov + email : ivan@opengis.ch + ***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + + +#ifndef QGSALGORITHMFIELDSCALCULATOR_H +#define QGSALGORITHMFIELDSCALCULATOR_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +/** + * Native field calculator algorithm. + */ +class QgsFieldCalculatorAlgorithm : public QgsProcessingFeatureBasedAlgorithm +{ + + public: + + QgsFieldCalculatorAlgorithm() = default; + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QList inputLayerTypes() const override; + QgsFieldCalculatorAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + void initParameters( const QVariantMap &configuration = QVariantMap() ) override; + QString outputName() const override; + QgsFields outputFields( const QgsFields &inputFields ) const override; + QgsProcessingFeatureSource::Flag sourceFlags() const override; + + bool prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + QgsFeatureList processFeature( const QgsFeature &feature, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + bool supportInPlaceEdit( const QgsMapLayer *layer ) const override; + + private: + QgsFields mFields; + int mFieldIdx; + QgsExpression mExpression; + QgsExpressionContext mExpressionContext; + QgsDistanceArea mDa; + int mRowNumber; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMFIELDSCALCULATOR_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index fbcb60f0643..261ec47382c 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -68,6 +68,7 @@ #include "qgsalgorithmextractvertices.h" #include "qgsalgorithmextractspecificvertices.h" #include "qgsalgorithmextractzmvalues.h" +#include "qgsalgorithmfieldcalculator.h" #include "qgsalgorithmfiledownloader.h" #include "qgsalgorithmfillnodata.h" #include "qgsalgorithmfilter.h" @@ -292,6 +293,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsExtractVerticesAlgorithm() ); addAlgorithm( new QgsExtractSpecificVerticesAlgorithm() ); addAlgorithm( new QgsExtractZValuesAlgorithm() ); + addAlgorithm( new QgsFieldCalculatorAlgorithm() ); addAlgorithm( new QgsFileDownloaderAlgorithm() ); addAlgorithm( new QgsFillNoDataAlgorithm() ); addAlgorithm( new QgsFilterAlgorithm() ); diff --git a/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp b/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp index 00801d90b16..8bb095ebf88 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp +++ b/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp @@ -30,6 +30,7 @@ #include "qgssettings.h" #include "qgsexpressionlineedit.h" #include "qgsfieldexpressionwidget.h" +#include "qgsexpressionbuilderwidget.h" #include "qgsprocessingmultipleselectiondialog.h" #include "qgslayoutmanager.h" #include "qgsproject.h" @@ -1943,15 +1944,30 @@ QWidget *QgsProcessingExpressionWidgetWrapper::createWidget() } else { - mFieldExpWidget = new QgsFieldExpressionWidget(); - mFieldExpWidget->setToolTip( parameterDefinition()->toolTip() ); - mFieldExpWidget->setExpressionDialogTitle( parameterDefinition()->description() ); - mFieldExpWidget->registerExpressionContextGenerator( this ); - connect( mFieldExpWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString & ) >( &QgsFieldExpressionWidget::fieldChanged ), this, [ = ]( const QString & ) + if ( expParam->metadata().value( QStringLiteral( "inlineEditor" ) ).toBool() ) { - emit widgetValueHasChanged( this ); - } ); - return mFieldExpWidget; + mExpBuilderWidget = new QgsExpressionBuilderWidget(); + mExpBuilderWidget->setToolTip( parameterDefinition()->toolTip() ); + mExpBuilderWidget->init( createExpressionContext() ); + connect( mExpBuilderWidget, &QgsExpressionBuilderWidget::expressionParsed, this, [ = ]( bool changed ) + { + Q_UNUSED( changed ); + emit widgetValueHasChanged( this ); + } ); + return mExpBuilderWidget; + } + else + { + mFieldExpWidget = new QgsFieldExpressionWidget(); + mFieldExpWidget->setToolTip( parameterDefinition()->toolTip() ); + mFieldExpWidget->setExpressionDialogTitle( parameterDefinition()->description() ); + mFieldExpWidget->registerExpressionContextGenerator( this ); + connect( mFieldExpWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString & ) >( &QgsFieldExpressionWidget::fieldChanged ), this, [ = ]( const QString & ) + { + emit widgetValueHasChanged( this ); + } ); + return mFieldExpWidget; + } } } } @@ -2005,6 +2021,8 @@ void QgsProcessingExpressionWidgetWrapper::setParentLayerWrapperValue( const Qgs { if ( mFieldExpWidget ) mFieldExpWidget->setLayer( nullptr ); + else if ( mExpBuilderWidget ) + mExpBuilderWidget->setLayer( nullptr ); else if ( mExpLineEdit ) mExpLineEdit->setLayer( nullptr ); return; @@ -2025,6 +2043,8 @@ void QgsProcessingExpressionWidgetWrapper::setParentLayerWrapperValue( const Qgs if ( mFieldExpWidget ) mFieldExpWidget->setLayer( layer ); + if ( mExpBuilderWidget ) + mExpBuilderWidget->setLayer( layer ); else if ( mExpLineEdit ) mExpLineEdit->setLayer( layer ); } @@ -2034,6 +2054,8 @@ void QgsProcessingExpressionWidgetWrapper::setWidgetValue( const QVariant &value const QString v = QgsProcessingParameters::parameterAsString( parameterDefinition(), value, context ); if ( mFieldExpWidget ) mFieldExpWidget->setExpression( v ); + else if ( mExpBuilderWidget ) + mExpBuilderWidget->setExpressionText( v ); else if ( mExpLineEdit ) mExpLineEdit->setExpression( v ); } @@ -2042,6 +2064,8 @@ QVariant QgsProcessingExpressionWidgetWrapper::widgetValue() const { if ( mFieldExpWidget ) return mFieldExpWidget->expression(); + if ( mExpBuilderWidget ) + return mExpBuilderWidget->expressionText(); else if ( mExpLineEdit ) return mExpLineEdit->expression(); else @@ -2076,6 +2100,9 @@ const QgsVectorLayer *QgsProcessingExpressionWidgetWrapper::linkedVectorLayer() if ( mFieldExpWidget && mFieldExpWidget->layer() ) return mFieldExpWidget->layer(); + if ( mExpBuilderWidget && mExpBuilderWidget->layer() ) + return mExpBuilderWidget->layer(); + return QgsAbstractProcessingParameterWidgetWrapper::linkedVectorLayer(); } diff --git a/src/gui/processing/qgsprocessingwidgetwrapperimpl.h b/src/gui/processing/qgsprocessingwidgetwrapperimpl.h index 793b3502721..6b9e5180fcb 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapperimpl.h +++ b/src/gui/processing/qgsprocessingwidgetwrapperimpl.h @@ -41,6 +41,7 @@ class QgsAuthConfigSelect; class QgsProcessingMatrixParameterPanel; class QgsFileWidget; class QgsFieldExpressionWidget; +class QgsExpressionBuilderWidget; class QgsExpressionLineEdit; class QgsProcessingParameterEnum; class QgsLayoutComboBox; @@ -677,6 +678,7 @@ class GUI_EXPORT QgsProcessingExpressionWidgetWrapper : public QgsAbstractProces private: QgsFieldExpressionWidget *mFieldExpWidget = nullptr; + QgsExpressionBuilderWidget *mExpBuilderWidget = nullptr; QgsExpressionLineEdit *mExpLineEdit = nullptr; std::unique_ptr< QgsVectorLayer > mParentLayer;