Skip to content

Commit

Permalink
Function to make output features compatible
Browse files Browse the repository at this point in the history
  • Loading branch information
elpaso authored and nyalldawson committed Sep 14, 2018
1 parent d8d32ac commit 11aaf90
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 15 deletions.
163 changes: 148 additions & 15 deletions python/plugins/processing/gui/AlgorithmExecutor.py
Expand Up @@ -34,7 +34,12 @@
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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -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)
Expand Down

0 comments on commit 11aaf90

Please sign in to comment.