This Processing Script is made available under the CC-0 license.
Remove Contained Polygons
from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
QgsFeatureSink,
QgsProcessingException,
QgsProcessingAlgorithm,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterField,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterBoolean,
QgsProcessingContext,
QgsProcessingFeedback,
QgsFeature,
QgsGeometry)
from collections import defaultdict
class RemoveContainedPolygonsAlgorithm(QgsProcessingAlgorithm):
"""
Remove polygons that are completely contained within other polygons.
Optionally group by field value to compare only polygons with the same value.
"""
# Parameter constants
INPUT = 'INPUT'
FIELD = 'FIELD'
OUTPUT = 'OUTPUT'
DETAILED_LOG = 'DETAILED_LOG'
def tr(self, string):
"""
Returns a translatable string using the Qt translation API.
"""
return QCoreApplication.translate('Processing', string)
def createInstance(self):
return RemoveContainedPolygonsAlgorithm()
def name(self):
"""
Returns the algorithm name, used for identifying the algorithm.
"""
return 'removecontainedpolygons'
def displayName(self):
"""
Returns the translated algorithm name.
"""
return self.tr('Remove Contained Polygons')
def group(self):
"""
Returns the name of the group this algorithm belongs to.
"""
return self.tr('Vector geometry')
def groupId(self):
"""
Returns the unique ID of the group this algorithm belongs to.
"""
return 'vectorgeometry'
def shortHelpString(self):
"""
Returns a localised short helper string for the algorithm.
"""
return self.tr("""
Remove polygons that are completely contained within other polygons.
Parameters:
- Input layer: Polygon layer to process
- Grouping field: Optional field to group polygons by (e.g., 'year', 'type')
- Show detailed statistics: Display detailed statistics by group
If a grouping field is selected, the algorithm compares each polygon only \
with other polygons that have the same value in the specified field.
If no grouping field is selected, all polygons are compared with each other.
Polygons that are completely contained within others are removed from the output.
""")
def initAlgorithm(self, config=None):
"""
Define the inputs and outputs of the algorithm.
"""
# Input parameter: polygon layer
self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT,
self.tr('Input layer'),
[QgsProcessing.TypeVectorPolygon]
)
)
# Optional parameter: grouping field
self.addParameter(
QgsProcessingParameterField(
self.FIELD,
self.tr('Grouping field (optional)'),
parentLayerParameterName=self.INPUT,
type=QgsProcessingParameterField.Any,
optional=True
)
)
# Parameter: detailed log
self.addParameter(
QgsProcessingParameterBoolean(
self.DETAILED_LOG,
self.tr('Show detailed statistics'),
defaultValue=True
)
)
# Output parameter
self.addParameter(
QgsProcessingParameterFeatureSink(
self.OUTPUT,
self.tr('Output layer')
)
)
def processAlgorithm(self, parameters, context, feedback):
"""
Execute the algorithm.
"""
# Get parameters
source = self.parameterAsSource(parameters, self.INPUT, context)
field_name = self.parameterAsString(parameters, self.FIELD, context)
detailed_log = self.parameterAsBool(parameters, self.DETAILED_LOG, context)
if source is None:
raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))
# Check if grouping field is specified and exists
use_grouping = bool(field_name)
if use_grouping:
field_index = source.fields().lookupField(field_name)
if field_index == -1:
raise QgsProcessingException(f'Field "{field_name}" not found in input layer')
feedback.pushInfo(f"Using grouping field: '{field_name}'")
else:
feedback.pushInfo("No grouping field specified - comparing all polygons")
# Create output sink
(sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
source.fields(), source.wkbType(),
source.sourceCrs())
if sink is None:
raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT))
# Get all features
features = list(source.getFeatures())
total_features = len(features)
if total_features == 0:
feedback.pushInfo("No features found in input layer")
return {self.OUTPUT: dest_id}
feedback.pushInfo(f"Processing {total_features} features...")
# Group features by field value or put all in one group
if use_grouping:
features_by_group = defaultdict(list)
for feature in features:
group_value = feature[field_name]
# Handle None values
if group_value is None:
group_value = "NULL"
features_by_group[group_value].append(feature)
if detailed_log:
feedback.pushInfo(f"Groups found: {sorted(features_by_group.keys())}")
else:
# Put all features in a single group
features_by_group = {"ALL": features}
# Process each group separately
features_to_keep = []
removal_stats = {}
total_steps = sum(len(group_features) for group_features in features_by_group.values())
current_step = 0
for group_value in sorted(features_by_group.keys()):
group_features = features_by_group[group_value]
group_kept = []
group_removed_count = 0
if detailed_log and use_grouping:
feedback.pushInfo(f"\n--- Group: {group_value} ---")
feedback.pushInfo(f"Polygons in group: {len(group_features)}")
elif detailed_log:
feedback.pushInfo(f"\n--- Processing all polygons ---")
feedback.pushInfo(f"Total polygons: {len(group_features)}")
# Compare polygons within the same group
for i, feature1 in enumerate(group_features):
# Check if processing was cancelled
if feedback.isCanceled():
break
# Update progress
current_step += 1
feedback.setProgress(int(current_step / total_steps * 100))
geom1 = feature1.geometry()
is_contained = False
# Check if contained within any other polygon in the group
for j, feature2 in enumerate(group_features):
if i != j:
geom2 = feature2.geometry()
if geom1.within(geom2):
is_contained = True
group_removed_count += 1
break
if not is_contained:
group_kept.append(feature1)
# Group statistics
kept_count = len(group_kept)
removal_stats[group_value] = {
'original': len(group_features),
'kept': kept_count,
'removed': group_removed_count
}
features_to_keep.extend(group_kept)
if detailed_log:
feedback.pushInfo(f"Kept: {kept_count}")
feedback.pushInfo(f"Removed: {group_removed_count}")
if len(group_features) > 0:
feedback.pushInfo(f"Removal rate: {(group_removed_count/len(group_features)*100):.1f}%")
# Check if processing was cancelled
if feedback.isCanceled():
return {}
# Final summary
total_original = sum(stats['original'] for stats in removal_stats.values())
total_kept = sum(stats['kept'] for stats in removal_stats.values())
total_removed = sum(stats['removed'] for stats in removal_stats.values())
feedback.pushInfo(f"\n=== SUMMARY ===")
feedback.pushInfo(f"Original total: {total_original}")
feedback.pushInfo(f"Kept total: {total_kept}")
feedback.pushInfo(f"Removed total: {total_removed}")
if total_original > 0:
feedback.pushInfo(f"Overall removal rate: {(total_removed/total_original*100):.1f}%")
# Add features to output sink
for feature in features_to_keep:
if feedback.isCanceled():
break
sink.addFeature(feature, QgsFeatureSink.FastInsert)
feedback.pushInfo(f"\nProcessing completed! {len(features_to_keep)} polygons in output layer.")
return {self.OUTPUT: dest_id}
Automatically removes polygons that are completely contained within other polygons
Key Features: - Removes polygons completely contained within others; - Optional grouping by field (year, type, category, etc.); - Preserves all original attributes; - Shows detailed processing statistics; - Progress bar and cancellation support; - Compatible with any polygon layer format.
Thank you very much!
Reviewed by gabrieldeluca 1 week, 1 day ago
This Processing Script is made available under the CC-0 license.