QGIS API Documentation 4.3.0-Master (ffcfc20b9b4)
Loading...
Searching...
No Matches
qgsalgorithmdeleteduplicategeometries.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmdeleteduplicategeometries.cpp
3 -----------------------------------------
4 begin : December 2019
5 copyright : (C) 2019 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19
20#include "qgsgeometryengine.h"
21#include "qgsspatialindex.h"
22#include "qgsvectorlayer.h"
23
24#include <QString>
25
26using namespace Qt::StringLiterals;
27
29
30QString QgsDeleteDuplicateGeometriesAlgorithm::name() const
31{
32 return u"deleteduplicategeometries"_s;
33}
34
35QString QgsDeleteDuplicateGeometriesAlgorithm::displayName() const
36{
37 return QObject::tr( "Delete duplicate geometries" );
38}
39
40QStringList QgsDeleteDuplicateGeometriesAlgorithm::tags() const
41{
42 return QObject::tr( "drop,remove,same,points,coincident,overlapping,filter" ).split( ',' );
43}
44
45QString QgsDeleteDuplicateGeometriesAlgorithm::group() const
46{
47 return QObject::tr( "Vector general" );
48}
49
50QString QgsDeleteDuplicateGeometriesAlgorithm::groupId() const
51{
52 return u"vectorgeneral"_s;
53}
54
55void QgsDeleteDuplicateGeometriesAlgorithm::initAlgorithm( const QVariantMap & )
56{
57 addParameter( new QgsProcessingParameterFeatureSource( u"INPUT"_s, QObject::tr( "Input layer" ) ) );
58 addParameter( new QgsProcessingParameterFeatureSink( u"OUTPUT"_s, QObject::tr( "Cleaned" ) ) );
59
60 QgsProcessingParameterFeatureSink *duplicatesOutput = new QgsProcessingParameterFeatureSink( u"DUPLICATES"_s, QObject::tr( "Duplicates" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true );
61 duplicatesOutput->setCreateByDefault( false );
62 addParameter( duplicatesOutput );
63
64 addOutput( new QgsProcessingOutputNumber( u"RETAINED_COUNT"_s, QObject::tr( "Count of retained records" ) ) );
65 addOutput( new QgsProcessingOutputNumber( u"DUPLICATE_COUNT"_s, QObject::tr( "Count of discarded duplicate records" ) ) );
66}
67
68QString QgsDeleteDuplicateGeometriesAlgorithm::shortHelpString() const
69{
70 return QObject::tr(
71 "This algorithm finds duplicated geometries and removes them.\n\nAttributes are not checked, "
72 "so in case two features have identical geometries but different attributes, only one of "
73 "them will be added to the result layer.\n\n"
74 "Optionally, these duplicate features can be saved to a separate output for analysis."
75 );
76}
77
78QString QgsDeleteDuplicateGeometriesAlgorithm::shortDescription() const
79{
80 return QObject::tr( "Finds duplicated geometries in a layer and removes them." );
81}
82
83QgsDeleteDuplicateGeometriesAlgorithm *QgsDeleteDuplicateGeometriesAlgorithm::createInstance() const
84{
85 return new QgsDeleteDuplicateGeometriesAlgorithm();
86}
87
88bool QgsDeleteDuplicateGeometriesAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
89{
90 mSource.reset( parameterAsSource( parameters, u"INPUT"_s, context ) );
91 if ( !mSource )
92 throw QgsProcessingException( invalidSourceError( parameters, u"INPUT"_s ) );
93
94 return true;
95}
96
97QVariantMap QgsDeleteDuplicateGeometriesAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
98{
99 QGS_MARK_ALGORITHM_SOURCE
100
101 QString destId;
102 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u"OUTPUT"_s, context, destId, mSource->fields(), mSource->wkbType(), mSource->sourceCrs() ) );
103 if ( !sink )
104 throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) );
105
106 QString dupesSinkId;
107 std::unique_ptr<QgsFeatureSink> dupesSink( parameterAsSink( parameters, u"DUPLICATES"_s, context, dupesSinkId, mSource->fields(), mSource->wkbType(), mSource->sourceCrs() ) );
108
110
111 double step = mSource->featureCount() > 0 ? 100.0 / mSource->featureCount() : 0;
112 QHash<QgsFeatureId, QgsGeometry> geometries;
113 QSet<QgsFeatureId> nullGeometryFeatures;
114 long current = 0;
115 const QgsSpatialIndex index( it, [&]( const QgsFeature &f ) -> bool {
116 if ( feedback->isCanceled() )
117 return false;
118
119 if ( !f.hasGeometry() )
120 {
121 nullGeometryFeatures.insert( f.id() );
122 }
123 else
124 {
125 geometries.insert( f.id(), f.geometry() );
126 }
127
128 // overall this loop takes about 10% of time
129 current++;
130 feedback->setProgress( 0.10 * static_cast<double>( current ) * step );
131 return true;
132 } );
133
134 QgsFeature f;
135
136 // start by assuming everything is unique, and chop away at this list
137 QHash<QgsFeatureId, QgsGeometry> uniqueFeatures = geometries;
138 QHash<QgsFeatureId, QgsGeometry> duplicateFeatures;
139 current = 0;
140 long removed = 0;
141
142 for ( auto it = geometries.constBegin(); it != geometries.constEnd(); ++it )
143 {
144 const QgsFeatureId featureId = it.key();
145 const QgsGeometry geometry = it.value();
146
147 if ( feedback->isCanceled() )
148 break;
149
150 if ( !uniqueFeatures.contains( featureId ) )
151 {
152 // feature was already marked as a duplicate
153 }
154 else
155 {
156 const QList<QgsFeatureId> candidates = index.intersects( geometry.boundingBox() );
157
158 for ( const QgsFeatureId candidateId : candidates )
159 {
160 if ( candidateId == featureId )
161 continue;
162
163 if ( !uniqueFeatures.contains( candidateId ) )
164 {
165 // candidate already marked as a duplicate (not sure if this is possible,
166 // since it would mean the current feature would also have to be a duplicate!
167 // but let's be safe!)
168 continue;
169 }
170
171 const QgsGeometry candidateGeom = geometries.value( candidateId );
172 if ( geometry.isTopologicallyEqual( candidateGeom ) )
173 {
174 // candidate is a duplicate of feature
175 uniqueFeatures.remove( candidateId );
176 if ( dupesSink )
177 {
178 duplicateFeatures.insert( candidateId, candidateGeom );
179 }
180 removed++;
181 }
182 }
183 }
184
185 current++;
186 feedback->setProgress( 0.80 * static_cast<double>( current ) * step + 10 ); // takes about 80% of time
187 }
188
189 // now, fetch all the feature attributes for the unique features only
190 // be super-smart and don't re-fetch geometries
191 QSet<QgsFeatureId> outputFeatureIds = qgis::listToSet( uniqueFeatures.keys() );
192 outputFeatureIds.unite( nullGeometryFeatures );
193 step = outputFeatureIds.empty() ? 1 : 100.0 / outputFeatureIds.size();
194 const double stepTime = dupesSink ? 0.05 : 0.10;
195
197 it = mSource->getFeatures( request, Qgis::ProcessingFeatureSourceFlag::SkipGeometryValidityChecks );
198 current = 0;
199 while ( it.nextFeature( f ) )
200 {
201 if ( feedback->isCanceled() )
202 break;
203
204 // use already fetched geometry
205 if ( !nullGeometryFeatures.contains( f.id() ) )
206 {
207 f.setGeometry( uniqueFeatures.value( f.id() ) );
208 }
209 if ( !sink->addFeature( f, QgsFeatureSink::FastInsert ) )
210 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
211 else
212 feedback->featureAddedToSink( u"OUTPUT"_s );
213
214 current++;
215 feedback->setProgress( stepTime * static_cast<double>( current ) * step + 90 ); // takes about 5%-10% of time
216 }
217
218 feedback->pushInfo( QObject::tr( "%n duplicate feature(s) removed", nullptr, removed ) );
219
220 sink->finalize();
221 feedback->featureSinkFinalized( u"OUTPUT"_s );
222
223 if ( dupesSink )
224 {
225 // now, fetch all the feature attributes for the duplicate features
226 QSet<QgsFeatureId> duplicateFeatureIds = qgis::listToSet( duplicateFeatures.keys() );
227 step = duplicateFeatureIds.empty() ? 1 : 100.0 / duplicateFeatureIds.size();
228
230 it = mSource->getFeatures( request, Qgis::ProcessingFeatureSourceFlag::SkipGeometryValidityChecks );
231 current = 0;
232 while ( it.nextFeature( f ) )
233 {
234 if ( feedback->isCanceled() )
235 break;
236
237 // use already fetched geometry
238 f.setGeometry( duplicateFeatures.value( f.id() ) );
239 if ( !dupesSink->addFeature( f, QgsFeatureSink::FastInsert ) )
240 throw QgsProcessingException( writeFeatureError( dupesSink.get(), parameters, u"DUPLICATES"_s ) );
241 else
242 feedback->featureAddedToSink( u"DUPLICATES"_s );
243
244 current++;
245 feedback->setProgress( 0.05 * static_cast<double>( current ) * step + 95 ); // takes about 5% of time
246 }
247
248 dupesSink->finalize();
249 feedback->featureSinkFinalized( u"DUPLICATES"_s );
250 }
251
252 QVariantMap outputs;
253 outputs.insert( u"OUTPUT"_s, destId );
254 outputs.insert( u"DUPLICATE_COUNT"_s, static_cast<long long>( removed ) );
255 outputs.insert( u"RETAINED_COUNT"_s, outputFeatureIds.size() );
256 if ( dupesSink )
257 {
258 outputs.insert( u"DUPLICATES"_s, dupesSinkId );
259 }
260 return outputs;
261}
262
@ VectorAnyGeometry
Any vector layer with geometry.
Definition qgis.h:3749
@ NoGeometry
Geometry is not required. It may still be returned if e.g. required for a filter condition.
Definition qgis.h:2360
@ SkipGeometryValidityChecks
Invalid geometry checks should always be skipped. This flag can be useful for algorithms which always...
Definition qgis.h:3930
Wrapper for iterator of features from vector data provider or vector layer.
bool nextFeature(QgsFeature &f)
Fetch next feature and stores in f, returns true on success.
Wraps a request for features to a vector layer (or directly its vector data provider).
QgsFeatureRequest & setFlags(Qgis::FeatureRequestFlags flags)
Sets flags that affect how features will be fetched.
QgsFeatureRequest & setFilterFids(const QgsFeatureIds &fids)
Sets the feature IDs that should be fetched.
@ FastInsert
Use faster inserts, at the cost of updating the passed features to reflect changes made at the provid...
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition qgsfeature.h:60
QgsFeatureId id
Definition qgsfeature.h:63
QgsGeometry geometry
Definition qgsfeature.h:66
bool hasGeometry() const
Returns true if the feature has an associated geometry.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:56
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:65
A geometry is the spatial representation of a feature.
bool isTopologicallyEqual(const QgsGeometry &geometry, Qgis::GeometryBackend backend=Qgis::GeometryBackend::GEOS) const
Compares the geometry with another geometry using the specified backend.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
Contains information about the context in which a processing algorithm is executed.
void setCreateByDefault(bool createByDefault)
Sets whether the destination should be created by default.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
void featureAddedToSink(const QString &output)
Reports that a feature was added to the the sink associated with the specified algorithm output.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
void featureSinkFinalized(const QString &output)
Reports that a feature sink has been finalized.
A numeric output for processing algorithms.
A feature sink output for processing algorithms.
An input feature source (such as vector layers) parameter for processing algorithms.
A spatial index for QgsFeature objects.
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
QList< int > QgsAttributeList
Definition qgsfield.h:30