Function to make output features compatible

This commit is contained in:
Alessandro Pasotti 2018-09-10 08:09:52 +02:00 committed by Nyall Dawson
parent d8d32ac810
commit 11aaf90393
2 changed files with 149 additions and 15 deletions

View File

@ -34,7 +34,12 @@ from qgis.core import (Qgis,
QgsMessageLog,
QgsProcessingException,
QgsProcessingFeatureSourceDefinition,
QgsProcessingParameters)
QgsProcessingParameters,
QgsProject,
QgsFeatureRequest,
QgsFeature,
QgsExpression,
QgsWkbTypes)
from processing.gui.Postprocessing import handleAlgorithmResults
from processing.tools import dataobjects
from qgis.utils import iface
@ -63,40 +68,168 @@ def execute(alg, parameters, context=None, feedback=None):
return False, {}
def execute_in_place(alg, parameters, context=None, feedback=None):
def make_feature_compatible(new_features, input_layer):
"""Try to make new features compatible with old feature by:
- converting single to multi part
- dropping additional attributes
- adding back M/Z values
:param new_features: new features
:type new_features: list of QgsFeatures
:param input_layer: input layer
:type input_layer: QgsVectorLayer
:return: modified features
:rtype: list of QgsFeatures
"""
result_features = []
for new_f in new_features:
if (new_f.geometry().wkbType() != input_layer.wkbType() and
QgsWkbTypes.isMultiType(input_layer.wkbType()) and not
new_f.geometry().isMultipart()):
new_geom = new_f.geometry()
new_geom.convertToMultiType()
new_f.setGeometry(new_geom)
if len(new_f.fields()) > len(input_layer.fields()):
f = QgsFeature(input_layer.fields())
f.setGeometry(new_f.geometry())
f.setAttributes(new_f.attributes()[:len(input_layer.fields())])
result_features.append(new_f)
return result_features
def execute_in_place_run(alg, parameters, context=None, feedback=None):
"""Executes an algorithm modifying features in-place in the input layer.
The input layer must be editable or an exception is raised.
:param alg: algorithm to run
:type alg: QgsProcessingAlgorithm
:param parameters: parameters of the algorithm
:type parameters: dict
:param context: context, defaults to None
:param context: QgsProcessingContext, optional
:param feedback: feedback, defaults to None
:param feedback: QgsProcessingFeedback, optional
:raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
:return: a tuple with true if success and results
:rtype: tuple
"""
if feedback is None:
feedback = QgsProcessingFeedback()
if context is None:
context = dataobjects.createContext(feedback)
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True)
# It would be nicer to get the layer from INPUT with
# alg.parameterAsVectorLayer(parameters, 'INPUT', context)
# but it does not work.
active_layer_id, ok = parameters['INPUT'].source.value(context.expressionContext())
if ok:
active_layer = QgsProject.instance().mapLayer(active_layer_id)
if active_layer is None or not active_layer.isEditable():
raise QgsProcessingException(tr("Layer is not editable or layer with id '%s' could not be found in the current project.") % active_layer_id)
else:
return False, {}
parameters['OUTPUT'] = 'memory:'
try:
results, ok = alg.run(parameters, context, feedback)
new_feature_ids = []
layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
iface.activeLayer().beginEditCommand('Edit features')
iface.activeLayer().deleteFeatures(iface.activeLayer().selectedFeatureIds())
features = []
for f in layer.getFeatures():
features.append(f)
iface.activeLayer().addFeatures(features)
new_selection = [f.id() for f in features]
iface.activeLayer().endEditCommand()
#iface.activeLayer().selectByIds(new_selection)
iface.activeLayer().triggerRepaint()
active_layer.beginEditCommand(tr('In-place editing by %s') % alg.name())
req = QgsFeatureRequest(QgsExpression(r"$id < 0"))
req.setFlags(QgsFeatureRequest.NoGeometry)
req.setSubsetOfAttributes([])
# Checks whether the algorithm has a processFeature method
if hasattr(alg, 'processFeature'): # in-place feature editing
alg.prepare(parameters, context, feedback)
field_idxs = range(len(active_layer.fields()))
feature_iterator = active_layer.getFeatures(QgsFeatureRequest(active_layer.selectedFeatureIds())) if parameters['INPUT'].selectedFeaturesOnly else active_layer.getFeatures()
for f in feature_iterator:
new_features = alg.processFeature(f, context, feedback)
new_features = make_feature_compatible(new_features, active_layer)
if len(new_features) == 0:
active_layer.deleteFeature(f.id())
elif len(new_features) == 1:
new_f = new_features[0]
if not f.geometry().equals(new_f.geometry()):
active_layer.changeGeometry(f.id(), new_f.geometry())
if f.attributes() != new_f.attributes():
active_layer.changeAttributeValues(f.id(), dict(zip(field_idxs, new_f.attributes())), dict(zip(field_idxs, f.attributes())))
new_feature_ids.append(f.id())
else:
active_layer.deleteFeature(f.id())
# Get the new ids
old_ids = set([f.id() for f in active_layer.getFeatures(req)])
active_layer.addFeatures(new_features)
new_ids = set([f.id() for f in active_layer.getFeatures(req)])
new_feature_ids += list(new_ids - old_ids)
results, ok = {}, True
else: # Traditional 'run' with delete and add features cycle
results, ok = alg.run(parameters, context, feedback)
result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context)
# TODO: check if features have changed before delete/add cycle
active_layer.deleteFeatures(active_layer.selectedFeatureIds())
new_features = []
for f in result_layer.getFeatures():
new_features.append(make_feature_compatible([f], active_layer))
# Get the new ids
old_ids = set([f.id() for f in active_layer.getFeatures(req)])
active_layer.addFeatures(new_features)
new_ids = set([f.id() for f in active_layer.getFeatures(req)])
new_feature_ids += list(new_ids - old_ids)
if ok and new_feature_ids:
active_layer.selectByIds(new_feature_ids)
elif not ok:
active_layer.rollback()
active_layer.endEditCommand()
return ok, results
except QgsProcessingException as e:
QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
if feedback is not None:
feedback.reportError(e.msg)
return False, {}
def execute_in_place(alg, parameters, context=None, feedback=None):
"""Executes an algorithm modifying features in-place in the active layer.
The input layer must be editable or an exception is raised.
:param alg: algorithm to run
:type alg: QgsProcessingAlgorithm
:param parameters: parameters of the algorithm
:type parameters: dict
:param context: context, defaults to None
:param context: QgsProcessingContext, optional
:param feedback: feedback, defaults to None
:param feedback: QgsProcessingFeedback, optional
:raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project
:return: a tuple with true if success and results
:rtype: tuple
"""
parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True)
ok, results = execute_in_place_run(alg, parameters, context=context, feedback=feedback)
if ok:
iface.activeLayer().triggerRepaint()
return ok, results
def executeIterating(alg, parameters, paramToIter, context, feedback):
# Generate all single-feature layers
parameter_definition = alg.parameterDefinition(paramToIter)

View File

@ -142,6 +142,7 @@ ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py)
ADD_PYTHON_TEST(PyQgsPointDisplacementRenderer test_qgspointdisplacementrenderer.py)
ADD_PYTHON_TEST(PyQgsPostgresDomain test_qgspostgresdomain.py)
ADD_PYTHON_TEST(PyQgsProcessingRecentAlgorithmLog test_qgsprocessingrecentalgorithmslog.py)
ADD_PYTHON_TEST(PyQgsProcessingInPlace test_qgsprocessinginplace.py)
ADD_PYTHON_TEST(PyQgsProjectionSelectionWidgets test_qgsprojectionselectionwidgets.py)
ADD_PYTHON_TEST(PyQgsProjectMetadata test_qgsprojectmetadata.py)
ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py)