|
34 | 34 | QgsMessageLog,
|
35 | 35 | QgsProcessingException,
|
36 | 36 | QgsProcessingFeatureSourceDefinition,
|
37 |
| - QgsProcessingParameters) |
| 37 | + QgsProcessingParameters, |
| 38 | + QgsProject, |
| 39 | + QgsFeatureRequest, |
| 40 | + QgsFeature, |
| 41 | + QgsExpression, |
| 42 | + QgsWkbTypes) |
38 | 43 | from processing.gui.Postprocessing import handleAlgorithmResults
|
39 | 44 | from processing.tools import dataobjects
|
40 | 45 | from qgis.utils import iface
|
@@ -63,40 +68,168 @@ def execute(alg, parameters, context=None, feedback=None):
|
63 | 68 | return False, {}
|
64 | 69 |
|
65 | 70 |
|
66 |
| -def execute_in_place(alg, parameters, context=None, feedback=None): |
| 71 | +def make_feature_compatible(new_features, input_layer): |
| 72 | + """Try to make new features compatible with old feature by: |
| 73 | +
|
| 74 | + - converting single to multi part |
| 75 | + - dropping additional attributes |
| 76 | + - adding back M/Z values |
| 77 | +
|
| 78 | +
|
| 79 | + :param new_features: new features |
| 80 | + :type new_features: list of QgsFeatures |
| 81 | + :param input_layer: input layer |
| 82 | + :type input_layer: QgsVectorLayer |
| 83 | + :return: modified features |
| 84 | + :rtype: list of QgsFeatures |
| 85 | + """ |
| 86 | + |
| 87 | + result_features = [] |
| 88 | + for new_f in new_features: |
| 89 | + if (new_f.geometry().wkbType() != input_layer.wkbType() and |
| 90 | + QgsWkbTypes.isMultiType(input_layer.wkbType()) and not |
| 91 | + new_f.geometry().isMultipart()): |
| 92 | + new_geom = new_f.geometry() |
| 93 | + new_geom.convertToMultiType() |
| 94 | + new_f.setGeometry(new_geom) |
| 95 | + if len(new_f.fields()) > len(input_layer.fields()): |
| 96 | + f = QgsFeature(input_layer.fields()) |
| 97 | + f.setGeometry(new_f.geometry()) |
| 98 | + f.setAttributes(new_f.attributes()[:len(input_layer.fields())]) |
| 99 | + result_features.append(new_f) |
| 100 | + return result_features |
| 101 | + |
| 102 | + |
| 103 | +def execute_in_place_run(alg, parameters, context=None, feedback=None): |
| 104 | + """Executes an algorithm modifying features in-place in the input layer. |
| 105 | +
|
| 106 | + The input layer must be editable or an exception is raised. |
| 107 | +
|
| 108 | + :param alg: algorithm to run |
| 109 | + :type alg: QgsProcessingAlgorithm |
| 110 | + :param parameters: parameters of the algorithm |
| 111 | + :type parameters: dict |
| 112 | + :param context: context, defaults to None |
| 113 | + :param context: QgsProcessingContext, optional |
| 114 | + :param feedback: feedback, defaults to None |
| 115 | + :param feedback: QgsProcessingFeedback, optional |
| 116 | + :raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project |
| 117 | + :return: a tuple with true if success and results |
| 118 | + :rtype: tuple |
| 119 | + """ |
67 | 120 |
|
68 | 121 | if feedback is None:
|
69 | 122 | feedback = QgsProcessingFeedback()
|
70 | 123 | if context is None:
|
71 | 124 | context = dataobjects.createContext(feedback)
|
72 | 125 |
|
73 |
| - parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True) |
| 126 | + # It would be nicer to get the layer from INPUT with |
| 127 | + # alg.parameterAsVectorLayer(parameters, 'INPUT', context) |
| 128 | + # but it does not work. |
| 129 | + active_layer_id, ok = parameters['INPUT'].source.value(context.expressionContext()) |
| 130 | + if ok: |
| 131 | + active_layer = QgsProject.instance().mapLayer(active_layer_id) |
| 132 | + if active_layer is None or not active_layer.isEditable(): |
| 133 | + raise QgsProcessingException(tr("Layer is not editable or layer with id '%s' could not be found in the current project.") % active_layer_id) |
| 134 | + else: |
| 135 | + return False, {} |
74 | 136 |
|
75 | 137 | parameters['OUTPUT'] = 'memory:'
|
76 | 138 |
|
77 | 139 | try:
|
78 |
| - results, ok = alg.run(parameters, context, feedback) |
| 140 | + new_feature_ids = [] |
79 | 141 |
|
80 |
| - layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context) |
81 |
| - iface.activeLayer().beginEditCommand('Edit features') |
82 |
| - iface.activeLayer().deleteFeatures(iface.activeLayer().selectedFeatureIds()) |
83 |
| - features = [] |
84 |
| - for f in layer.getFeatures(): |
85 |
| - features.append(f) |
86 |
| - iface.activeLayer().addFeatures(features) |
87 |
| - new_selection = [f.id() for f in features] |
88 |
| - iface.activeLayer().endEditCommand() |
89 |
| - #iface.activeLayer().selectByIds(new_selection) |
90 |
| - iface.activeLayer().triggerRepaint() |
| 142 | + active_layer.beginEditCommand(tr('In-place editing by %s') % alg.name()) |
| 143 | + |
| 144 | + req = QgsFeatureRequest(QgsExpression(r"$id < 0")) |
| 145 | + req.setFlags(QgsFeatureRequest.NoGeometry) |
| 146 | + req.setSubsetOfAttributes([]) |
| 147 | + |
| 148 | + # Checks whether the algorithm has a processFeature method |
| 149 | + if hasattr(alg, 'processFeature'): # in-place feature editing |
| 150 | + alg.prepare(parameters, context, feedback) |
| 151 | + field_idxs = range(len(active_layer.fields())) |
| 152 | + feature_iterator = active_layer.getFeatures(QgsFeatureRequest(active_layer.selectedFeatureIds())) if parameters['INPUT'].selectedFeaturesOnly else active_layer.getFeatures() |
| 153 | + for f in feature_iterator: |
| 154 | + new_features = alg.processFeature(f, context, feedback) |
| 155 | + new_features = make_feature_compatible(new_features, active_layer) |
| 156 | + if len(new_features) == 0: |
| 157 | + active_layer.deleteFeature(f.id()) |
| 158 | + elif len(new_features) == 1: |
| 159 | + new_f = new_features[0] |
| 160 | + if not f.geometry().equals(new_f.geometry()): |
| 161 | + active_layer.changeGeometry(f.id(), new_f.geometry()) |
| 162 | + if f.attributes() != new_f.attributes(): |
| 163 | + active_layer.changeAttributeValues(f.id(), dict(zip(field_idxs, new_f.attributes())), dict(zip(field_idxs, f.attributes()))) |
| 164 | + new_feature_ids.append(f.id()) |
| 165 | + else: |
| 166 | + active_layer.deleteFeature(f.id()) |
| 167 | + # Get the new ids |
| 168 | + old_ids = set([f.id() for f in active_layer.getFeatures(req)]) |
| 169 | + active_layer.addFeatures(new_features) |
| 170 | + new_ids = set([f.id() for f in active_layer.getFeatures(req)]) |
| 171 | + new_feature_ids += list(new_ids - old_ids) |
| 172 | + |
| 173 | + results, ok = {}, True |
| 174 | + |
| 175 | + else: # Traditional 'run' with delete and add features cycle |
| 176 | + results, ok = alg.run(parameters, context, feedback) |
| 177 | + |
| 178 | + result_layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context) |
| 179 | + # TODO: check if features have changed before delete/add cycle |
| 180 | + active_layer.deleteFeatures(active_layer.selectedFeatureIds()) |
| 181 | + new_features = [] |
| 182 | + for f in result_layer.getFeatures(): |
| 183 | + new_features.append(make_feature_compatible([f], active_layer)) |
| 184 | + |
| 185 | + # Get the new ids |
| 186 | + old_ids = set([f.id() for f in active_layer.getFeatures(req)]) |
| 187 | + active_layer.addFeatures(new_features) |
| 188 | + new_ids = set([f.id() for f in active_layer.getFeatures(req)]) |
| 189 | + new_feature_ids += list(new_ids - old_ids) |
| 190 | + |
| 191 | + if ok and new_feature_ids: |
| 192 | + active_layer.selectByIds(new_feature_ids) |
| 193 | + elif not ok: |
| 194 | + active_layer.rollback() |
| 195 | + |
| 196 | + active_layer.endEditCommand() |
91 | 197 |
|
92 | 198 | return ok, results
|
| 199 | + |
93 | 200 | except QgsProcessingException as e:
|
94 | 201 | QgsMessageLog.logMessage(str(sys.exc_info()[0]), 'Processing', Qgis.Critical)
|
95 | 202 | if feedback is not None:
|
96 | 203 | feedback.reportError(e.msg)
|
| 204 | + |
97 | 205 | return False, {}
|
98 | 206 |
|
99 | 207 |
|
| 208 | +def execute_in_place(alg, parameters, context=None, feedback=None): |
| 209 | + """Executes an algorithm modifying features in-place in the active layer. |
| 210 | +
|
| 211 | + The input layer must be editable or an exception is raised. |
| 212 | +
|
| 213 | + :param alg: algorithm to run |
| 214 | + :type alg: QgsProcessingAlgorithm |
| 215 | + :param parameters: parameters of the algorithm |
| 216 | + :type parameters: dict |
| 217 | + :param context: context, defaults to None |
| 218 | + :param context: QgsProcessingContext, optional |
| 219 | + :param feedback: feedback, defaults to None |
| 220 | + :param feedback: QgsProcessingFeedback, optional |
| 221 | + :raises QgsProcessingException: raised when the layer is not editable or the layer cannot be found in the current project |
| 222 | + :return: a tuple with true if success and results |
| 223 | + :rtype: tuple |
| 224 | + """ |
| 225 | + |
| 226 | + parameters['INPUT'] = QgsProcessingFeatureSourceDefinition(iface.activeLayer().id(), True) |
| 227 | + ok, results = execute_in_place_run(alg, parameters, context=context, feedback=feedback) |
| 228 | + if ok: |
| 229 | + iface.activeLayer().triggerRepaint() |
| 230 | + return ok, results |
| 231 | + |
| 232 | + |
100 | 233 | def executeIterating(alg, parameters, paramToIter, context, feedback):
|
101 | 234 | # Generate all single-feature layers
|
102 | 235 | parameter_definition = alg.parameterDefinition(paramToIter)
|
|
0 commit comments