Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions AppendFeaturesToLayer/processing/algs/AppendFeaturesToLayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ class AppendFeaturesToLayer(QgsProcessingAlgorithm):
NO_ACTION_TEXT = "Just APPEND all features, no matter of duplicates"
SKIP_FEATURE_TEXT = 'If duplicate is found, SKIP feature'
UPDATE_EXISTING_FEATURE_TEXT = 'If duplicate is found, UPDATE existing feature'
UPDATE_EXISTING_FEATURE_SKIP_NULL_TEXT = 'If duplicate is found, UPDATE existing feature BUT skip NULL values in source'
UPDATE_EXISTING_GEOMETRY_TEXT = "If duplicate is found, only UPDATE existing feature's geometry"
NO_ACTION = 0
SKIP_FEATURE = 1
UPDATE_EXISTING_FEATURE = 2
UPDATE_EXISTING_GEOMETRY = 3
UPDATE_EXISTING_FEATURE_SKIP_NULL = 3
UPDATE_EXISTING_GEOMETRY = 4


def createInstance(self):
return type(self)()
Expand All @@ -74,10 +77,20 @@ def __init__(self):
super().__init__()

def shortHelpString(self):
return QCoreApplication.translate("AppendFeaturesToLayer", "This algorithm copies features from a source layer into a target layer.\n\n"
"Field mapping is handled automatically. Fields that are in both source and target layers are copied. Fields that are only found in source are not copied to target layer.\n\n"
"Geometry conversion is done automatically, if required by the target layer. For instance, single-part geometries are converted to multi-part if target layer handles multi-geometries; polygons are converted to lines if target layer stores lines; among others.\n\n"
"This algorithm allows you to choose a field in source and target layers to compare and detect duplicates. It has 4 modes of operation: 1) APPEND feature, regardless of duplicates; 2) SKIP feature if duplicate is found; 3) UPDATE the feature in target layer with attributes (including geometry) from the feature in the source layer; or 4) Only UPDATE the feature's geometry in target layer (leaving attributes intact) if duplicate is found.")
return QCoreApplication.translate("AppendFeaturesToLayer",
"This algorithm copies features from a source layer into a target layer.\n\n"
"Field mapping is handled automatically. Fields that are in both source and target layers are copied. "
"Fields that are only found in source are not copied to target layer.\n\n"
"Geometry conversion is done automatically, if required by the target layer. For instance, single-part geometries "
"are converted to multi-part if target layer handles multi-geometries; polygons are converted to lines if target layer "
"stores lines; among others.\n\n"
"This algorithm allows you to choose a field in source and target layers to compare and detect duplicates. It has 5 modes of operation:\n"
"1) APPEND feature, regardless of duplicates;\n"
"2) SKIP feature if duplicate is found;\n"
"3) UPDATE the feature in target layer with attributes (including geometry) from the feature in the source layer;\n"
"4) UPDATE the feature in target layer, but skip fields from source that are NULL or empty;\n"
"5) Only UPDATE the feature's geometry in target layer (leaving attributes intact) if duplicate is found."
)

def initAlgorithm(self, config=None):
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
Expand All @@ -101,10 +114,11 @@ def initAlgorithm(self, config=None):
'Action for duplicate features'),
[self.NO_ACTION_TEXT, self.SKIP_FEATURE_TEXT,
self.UPDATE_EXISTING_FEATURE_TEXT,
self.UPDATE_EXISTING_FEATURE_SKIP_NULL_TEXT, # <-- HIER ergänzt
self.UPDATE_EXISTING_GEOMETRY_TEXT],
False,
self.NO_ACTION_TEXT,
optional=False))
optional=False))
self.addOutput(QgsProcessingOutputVectorLayer(self.OUTPUT,
QCoreApplication.translate("AppendFeaturesToLayer",
"Target layer to paste new features")))
Expand Down Expand Up @@ -133,6 +147,7 @@ def processAlgorithm(self, parameters, context, feedback):
target = self.parameterAsVectorLayer(parameters, self.OUTPUT, context)
target_fields_parameter = self.parameterAsFields(parameters, self.OUTPUT_FIELD, context)
action_on_duplicate = self.parameterAsEnum(parameters, self.ACTION_ON_DUPLICATE, context)
skip_null_values = action_on_duplicate == self.UPDATE_EXISTING_FEATURE_SKIP_NULL

results = {self.OUTPUT: None,
self.APPENDED_COUNT: None,
Expand Down Expand Up @@ -180,7 +195,7 @@ def processAlgorithm(self, parameters, context, feedback):
feedback.reportError("\nWARNING: The target layer does not support appending features to it! Choose another target layer.")
return results

if action_on_duplicate == self.UPDATE_EXISTING_FEATURE:
if action_on_duplicate in (self.UPDATE_EXISTING_FEATURE, self.UPDATE_EXISTING_FEATURE_SKIP_NULL):
if target.isSpatial() and not (
caps & QgsVectorDataProvider.ChangeAttributeValues and caps & QgsVectorDataProvider.ChangeGeometries):
feedback.reportError(
Expand Down Expand Up @@ -220,7 +235,7 @@ def processAlgorithm(self, parameters, context, feedback):
mapping = dict()
for target_idx in target.fields().allAttributesList():
# We won't update PKs on UPDATE mode, that would be dangerous (at least most of the times)!
if action_on_duplicate == self.UPDATE_EXISTING_FEATURE and target_idx in target.primaryKeyAttributes():
if action_on_duplicate in (self.UPDATE_EXISTING_FEATURE, self.UPDATE_EXISTING_FEATURE_SKIP_NULL) and target_idx in target.primaryKeyAttributes():
continue

# Check that we don't have an automatic PK.
Expand Down Expand Up @@ -303,11 +318,18 @@ def processAlgorithm(self, parameters, context, feedback):
# Avoid intersection if enabled in digitize settings
geom.avoidIntersections(QgsProject.instance().avoidIntersectionsLayers())

if target_feature_exists and action_on_duplicate in (self.UPDATE_EXISTING_FEATURE, self.UPDATE_EXISTING_GEOMETRY):
if target_feature_exists and action_on_duplicate in (self.UPDATE_EXISTING_FEATURE, self.UPDATE_EXISTING_FEATURE_SKIP_NULL, self.UPDATE_EXISTING_GEOMETRY):
for t_f in target.getFeatures(target_value_dict[duplicate_target_value]):
duplicate_features_set.add(t_f.id())
if action_on_duplicate == self.UPDATE_EXISTING_FEATURE:
updated_features[t_f.id()] = attrs
if action_on_duplicate in (self.UPDATE_EXISTING_FEATURE, self.UPDATE_EXISTING_FEATURE_SKIP_NULL):
if skip_null_values:
cleaned_attrs = {
k: v for k, v in attrs.items()
if v not in [None, 'NULL'] and (not isinstance(v, str) or v.strip() != '') and QVariant(v).isValid() and not QVariant(v).isNull()
}
updated_features[t_f.id()] = cleaned_attrs
else:
updated_features[t_f.id()] = attrs

if QgsWkbTypes.geometryType(source.wkbType()) != QgsWkbTypes.NullGeometry and target.isSpatial():
# Only overwrite geometry if both source and target layers are spatial
Expand All @@ -326,6 +348,8 @@ def processAlgorithm(self, parameters, context, feedback):

if updated_features:
for k, v in updated_features.items():
if not v: # v is an empty dict → nothing to update
continue
if target.changeAttributeValues(k, v):
updated_features_count += 1
else:
Expand Down Expand Up @@ -360,7 +384,7 @@ def processAlgorithm(self, parameters, context, feedback):
))
results[self.SKIPPED_COUNT] = skipped_features_count

if action_on_duplicate == self.UPDATE_EXISTING_FEATURE:
if action_on_duplicate in (self.UPDATE_EXISTING_FEATURE, self.UPDATE_EXISTING_FEATURE_SKIP_NULL):
feedback.pushInfo("\nUPDATED FEATURES: {} out of {} duplicate features were updated while copying features to '{}'!".format(
updated_features_count,
len(duplicate_features_set),
Expand Down
Loading