QGIS API Documentation 3.41.0-Master (af5edcb665c)
Loading...
Searching...
No Matches
qgsalgorithmdetectdatasetchanges.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmdetectdatasetchanges.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#include "qgsvectorlayer.h"
20#include "qgsspatialindex.h"
21
23
24QString QgsDetectVectorChangesAlgorithm::name() const
25{
26 return QStringLiteral( "detectvectorchanges" );
27}
28
29QString QgsDetectVectorChangesAlgorithm::displayName() const
30{
31 return QObject::tr( "Detect dataset changes" );
32}
33
34QStringList QgsDetectVectorChangesAlgorithm::tags() const
35{
36 return QObject::tr( "added,dropped,new,deleted,features,geometries,difference,delta,revised,original,version" ).split( ',' );
37}
38
39QString QgsDetectVectorChangesAlgorithm::group() const
40{
41 return QObject::tr( "Vector general" );
42}
43
44QString QgsDetectVectorChangesAlgorithm::groupId() const
45{
46 return QStringLiteral( "vectorgeneral" );
47}
48
49void QgsDetectVectorChangesAlgorithm::initAlgorithm( const QVariantMap & )
50{
51 addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "ORIGINAL" ), QObject::tr( "Original layer" ) ) );
52 addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "REVISED" ), QObject::tr( "Revised layer" ) ) );
53
54 std::unique_ptr<QgsProcessingParameterField> compareAttributesParam = std::make_unique<QgsProcessingParameterField>( QStringLiteral( "COMPARE_ATTRIBUTES" ), QObject::tr( "Attributes to consider for match (or none to compare geometry only)" ), QVariant(), QStringLiteral( "ORIGINAL" ), Qgis::ProcessingFieldParameterDataType::Any, true, true );
55 compareAttributesParam->setDefaultToAllFields( true );
56 addParameter( compareAttributesParam.release() );
57
58 std::unique_ptr<QgsProcessingParameterDefinition> matchTypeParam = std::make_unique<QgsProcessingParameterEnum>( QStringLiteral( "MATCH_TYPE" ), QObject::tr( "Geometry comparison behavior" ), QStringList() << QObject::tr( "Exact Match" ) << QObject::tr( "Tolerant Match (Topological Equality)" ), false, 1 );
59 matchTypeParam->setFlags( matchTypeParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
60 addParameter( matchTypeParam.release() );
61
62 addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "UNCHANGED" ), QObject::tr( "Unchanged features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, true ) );
63 addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "ADDED" ), QObject::tr( "Added features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, true ) );
64 addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "DELETED" ), QObject::tr( "Deleted features" ), Qgis::ProcessingSourceType::VectorAnyGeometry, QVariant(), true, true ) );
65
66 addOutput( new QgsProcessingOutputNumber( QStringLiteral( "UNCHANGED_COUNT" ), QObject::tr( "Count of unchanged features" ) ) );
67 addOutput( new QgsProcessingOutputNumber( QStringLiteral( "ADDED_COUNT" ), QObject::tr( "Count of features added in revised layer" ) ) );
68 addOutput( new QgsProcessingOutputNumber( QStringLiteral( "DELETED_COUNT" ), QObject::tr( "Count of features deleted from original layer" ) ) );
69}
70
71QString QgsDetectVectorChangesAlgorithm::shortHelpString() const
72{
73 return QObject::tr( "This algorithm compares two vector layers, and determines which features are unchanged, added or deleted between "
74 "the two. It is designed for comparing two different versions of the same dataset.\n\n"
75 "When comparing features, the original and revised feature geometries will be compared against each other. Depending "
76 "on the Geometry Comparison Behavior setting, the comparison will either be made using an exact comparison (where "
77 "geometries must be an exact match for each other, including the order and count of vertices) or a topological "
78 "comparison only (where geometries are considered equal if all of their component edges overlap. E.g. "
79 "lines with the same vertex locations but opposite direction will be considered equal by this method). If the topological "
80 "comparison is selected then any z or m values present in the geometries will not be compared.\n\n"
81 "By default, the algorithm compares all attributes from the original and revised features. If the Attributes to Consider for Match "
82 "parameter is changed, then only the selected attributes will be compared (e.g. allowing users to ignore a timestamp or ID field "
83 "which is expected to change between the revisions).\n\n"
84 "If any features in the original or revised layers do not have an associated geometry, then care must be taken to ensure "
85 "that these features have a unique set of attributes selected for comparison. If this condition is not met, warnings will be "
86 "raised and the resultant outputs may be misleading.\n\n"
87 "The algorithm outputs three layers, one containing all features which are considered to be unchanged between the revisions, "
88 "one containing features deleted from the original layer which are not present in the revised layer, and one containing features "
89 "added to the revised layer which are not present in the original layer." );
90}
91
92QString QgsDetectVectorChangesAlgorithm::shortDescription() const
93{
94 return QObject::tr( "Calculates features which are unchanged, added or deleted between two dataset versions." );
95}
96
97QgsDetectVectorChangesAlgorithm *QgsDetectVectorChangesAlgorithm::createInstance() const
98{
99 return new QgsDetectVectorChangesAlgorithm();
100}
101
102bool QgsDetectVectorChangesAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
103{
104 mOriginal.reset( parameterAsSource( parameters, QStringLiteral( "ORIGINAL" ), context ) );
105 if ( !mOriginal )
106 throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "ORIGINAL" ) ) );
107
108 mRevised.reset( parameterAsSource( parameters, QStringLiteral( "REVISED" ), context ) );
109 if ( !mRevised )
110 throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "REVISED" ) ) );
111
112 mMatchType = static_cast<GeometryMatchType>( parameterAsEnum( parameters, QStringLiteral( "MATCH_TYPE" ), context ) );
113
114 switch ( mMatchType )
115 {
116 case Exact:
117 if ( mOriginal->wkbType() != mRevised->wkbType() )
118 throw QgsProcessingException( QObject::tr( "Geometry type of revised layer (%1) does not match the original layer (%2). Consider using the \"Tolerant Match\" option instead." ).arg( QgsWkbTypes::displayString( mRevised->wkbType() ), QgsWkbTypes::displayString( mOriginal->wkbType() ) ) );
119 break;
120
121 case Topological:
122 if ( QgsWkbTypes::geometryType( mOriginal->wkbType() ) != QgsWkbTypes::geometryType( mRevised->wkbType() ) )
123 throw QgsProcessingException( QObject::tr( "Geometry type of revised layer (%1) does not match the original layer (%2)" ).arg( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( mRevised->wkbType() ) ), QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( mOriginal->wkbType() ) ) ) );
124 break;
125 }
126
127 if ( mOriginal->sourceCrs() != mRevised->sourceCrs() )
128 feedback->reportError( QObject::tr( "CRS for revised layer (%1) does not match the original layer (%2) - reprojection accuracy may affect geometry matching" ).arg( mOriginal->sourceCrs().userFriendlyIdentifier(), mRevised->sourceCrs().userFriendlyIdentifier() ), false );
129
130 mFieldsToCompare = parameterAsStrings( parameters, QStringLiteral( "COMPARE_ATTRIBUTES" ), context );
131 mOriginalFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
132 mRevisedFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
133 QStringList missingOriginalFields;
134 QStringList missingRevisedFields;
135 for ( const QString &field : mFieldsToCompare )
136 {
137 const int originalIndex = mOriginal->fields().lookupField( field );
138 mOriginalFieldsToCompareIndices.append( originalIndex );
139 if ( originalIndex < 0 )
140 missingOriginalFields << field;
141
142 const int revisedIndex = mRevised->fields().lookupField( field );
143 if ( revisedIndex < 0 )
144 missingRevisedFields << field;
145 mRevisedFieldsToCompareIndices.append( revisedIndex );
146 }
147
148 if ( !missingOriginalFields.empty() )
149 throw QgsProcessingException( QObject::tr( "Original layer missing selected comparison attributes: %1" ).arg( missingOriginalFields.join( ',' ) ) );
150 if ( !missingRevisedFields.empty() )
151 throw QgsProcessingException( QObject::tr( "Revised layer missing selected comparison attributes: %1" ).arg( missingRevisedFields.join( ',' ) ) );
152
153 return true;
154}
155
156QVariantMap QgsDetectVectorChangesAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
157{
158 QString unchangedDestId;
159 std::unique_ptr<QgsFeatureSink> unchangedSink( parameterAsSink( parameters, QStringLiteral( "UNCHANGED" ), context, unchangedDestId, mOriginal->fields(), mOriginal->wkbType(), mOriginal->sourceCrs() ) );
160 if ( !unchangedSink && parameters.value( QStringLiteral( "UNCHANGED" ) ).isValid() )
161 throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "UNCHANGED" ) ) );
162
163 QString addedDestId;
164 std::unique_ptr<QgsFeatureSink> addedSink( parameterAsSink( parameters, QStringLiteral( "ADDED" ), context, addedDestId, mRevised->fields(), mRevised->wkbType(), mRevised->sourceCrs() ) );
165 if ( !addedSink && parameters.value( QStringLiteral( "ADDED" ) ).isValid() )
166 throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "ADDED" ) ) );
167
168 QString deletedDestId;
169 std::unique_ptr<QgsFeatureSink> deletedSink( parameterAsSink( parameters, QStringLiteral( "DELETED" ), context, deletedDestId, mOriginal->fields(), mOriginal->wkbType(), mOriginal->sourceCrs() ) );
170 if ( !deletedSink && parameters.value( QStringLiteral( "DELETED" ) ).isValid() )
171 throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "DELETED" ) ) );
172
173 // first iteration: we loop through the entire original layer, building up a spatial index of ALL original geometries
174 // and collecting the original geometries themselves along with the attributes to compare
175 QgsFeatureRequest request;
176 request.setSubsetOfAttributes( mOriginalFieldsToCompareIndices );
177
178 QgsFeatureIterator it = mOriginal->getFeatures( request );
179
180 double step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
181 QHash<QgsFeatureId, QgsGeometry> originalGeometries;
182 QHash<QgsFeatureId, QgsAttributes> originalAttributes;
183 QHash<QgsAttributes, QgsFeatureId> originalNullGeometryAttributes;
184 long current = 0;
185
186 QgsAttributes attrs;
187 attrs.resize( mFieldsToCompare.size() );
188
189 const QgsSpatialIndex index( it, [&]( const QgsFeature &f ) -> bool {
190 if ( feedback->isCanceled() )
191 return false;
192
193 if ( f.hasGeometry() )
194 {
195 originalGeometries.insert( f.id(), f.geometry() );
196 }
197
198 if ( !mFieldsToCompare.empty() )
199 {
200 int idx = 0;
201 for ( const int field : mOriginalFieldsToCompareIndices )
202 {
203 attrs[idx++] = f.attributes().at( field );
204 }
205 originalAttributes.insert( f.id(), attrs );
206 }
207
208 if ( !f.hasGeometry() )
209 {
210 if ( originalNullGeometryAttributes.contains( attrs ) )
211 {
212 feedback->reportError( QObject::tr( "A non-unique set of comparison attributes was found for "
213 "one or more features without geometries - results may be misleading (features %1 and %2)" )
214 .arg( f.id() )
215 .arg( originalNullGeometryAttributes.value( attrs ) ) );
216 }
217 else
218 {
219 originalNullGeometryAttributes.insert( attrs, f.id() );
220 }
221 }
222
223 // overall this loop takes about 10% of time
224 current++;
225 feedback->setProgress( 0.10 * current * step );
226 return true;
227 } );
228
229 QSet<QgsFeatureId> unchangedOriginalIds;
230 QSet<QgsFeatureId> addedRevisedIds;
231 current = 0;
232
233 // second iteration: we loop through ALL revised features, checking whether each is a match for a geometry from the
234 // original set. If so, check if the feature is unchanged. If there's no match with the original features, we mark it as an "added" feature
235 step = mRevised->featureCount() > 0 ? 100.0 / mRevised->featureCount() : 0;
236 QgsFeatureRequest revisedRequest = QgsFeatureRequest().setDestinationCrs( mOriginal->sourceCrs(), context.transformContext() );
237 revisedRequest.setSubsetOfAttributes( mRevisedFieldsToCompareIndices );
238 it = mRevised->getFeatures( revisedRequest );
239 QgsFeature revisedFeature;
240 while ( it.nextFeature( revisedFeature ) )
241 {
242 if ( feedback->isCanceled() )
243 break;
244
245 int idx = 0;
246 for ( const int field : mRevisedFieldsToCompareIndices )
247 {
248 attrs[idx++] = revisedFeature.attributes().at( field );
249 }
250
251 bool matched = false;
252
253 if ( !revisedFeature.hasGeometry() )
254 {
255 if ( originalNullGeometryAttributes.contains( attrs ) )
256 {
257 // found a match for feature
258 unchangedOriginalIds.insert( originalNullGeometryAttributes.value( attrs ) );
259 matched = true;
260 }
261 }
262 else
263 {
264 // can we match this feature?
265 const QList<QgsFeatureId> candidates = index.intersects( revisedFeature.geometry().boundingBox() );
266
267 // lazy evaluate -- there may be NO candidates!
268 QgsGeometry revised;
269
270 for ( const QgsFeatureId candidateId : candidates )
271 {
272 if ( unchangedOriginalIds.contains( candidateId ) )
273 {
274 // already matched this original feature
275 continue;
276 }
277
278 // attribute comparison is faster to do first, if desired
279 if ( !mFieldsToCompare.empty() )
280 {
281 if ( attrs != originalAttributes[candidateId] )
282 {
283 // attributes don't match, so candidates is not a match
284 continue;
285 }
286 }
287
288 QgsGeometry original = originalGeometries.value( candidateId );
289 // lazy evaluation
290 if ( revised.isNull() )
291 {
292 revised = revisedFeature.geometry();
293 // drop z/m if not wanted for match
294 switch ( mMatchType )
295 {
296 case Topological:
297 {
298 revised.get()->dropMValue();
299 revised.get()->dropZValue();
300 original.get()->dropMValue();
301 original.get()->dropZValue();
302 break;
303 }
304
305 case Exact:
306 break;
307 }
308 }
309
310 bool geometryMatch = false;
311 switch ( mMatchType )
312 {
313 case Topological:
314 {
315 geometryMatch = revised.isGeosEqual( original );
316 break;
317 }
318
319 case Exact:
320 geometryMatch = revised.equals( original );
321 break;
322 }
323
324 if ( geometryMatch )
325 {
326 // candidate is a match for feature
327 unchangedOriginalIds.insert( candidateId );
328 matched = true;
329 break;
330 }
331 }
332 }
333
334 if ( !matched )
335 {
336 // new feature
337 addedRevisedIds.insert( revisedFeature.id() );
338 }
339
340 current++;
341 feedback->setProgress( 0.70 * current * step + 10 ); // takes about 70% of time
342 }
343
344 // third iteration: iterate back over the original features, and direct them to the appropriate sink.
345 // If they were marked as unchanged during the second iteration, we put them in the unchanged sink. Otherwise
346 // they are placed into the deleted sink.
347 step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
348
350 it = mOriginal->getFeatures( request );
351 current = 0;
352 long deleted = 0;
353 QgsFeature f;
354 while ( it.nextFeature( f ) )
355 {
356 if ( feedback->isCanceled() )
357 break;
358
359 // use already fetched geometry
360 f.setGeometry( originalGeometries.value( f.id(), QgsGeometry() ) );
361
362 if ( unchangedOriginalIds.contains( f.id() ) )
363 {
364 // unchanged
365 if ( unchangedSink )
366 {
367 if ( !unchangedSink->addFeature( f, QgsFeatureSink::FastInsert ) )
368 throw QgsProcessingException( writeFeatureError( unchangedSink.get(), parameters, QStringLiteral( "UNCHANGED" ) ) );
369 }
370 }
371 else
372 {
373 // deleted feature
374 if ( deletedSink )
375 {
376 if ( !deletedSink->addFeature( f, QgsFeatureSink::FastInsert ) )
377 throw QgsProcessingException( writeFeatureError( deletedSink.get(), parameters, QStringLiteral( "DELETED" ) ) );
378 }
379 deleted++;
380 }
381
382 current++;
383 feedback->setProgress( 0.10 * current * step + 80 ); // takes about 10% of time
384 }
385
386 // forth iteration: collect all added features and add them to the added sink
387 // NOTE: while we could potentially do this as part of the second iteration and save some time, we instead
388 // do this here using a brand new request because the second iteration
389 // is fetching reprojected features and we ideally want geometries from the revised layer's actual CRS only here!
390 // also, the second iteration is only fetching the actual attributes used in the comparison, whereas we want
391 // to include all attributes in the "added" output
392 if ( addedSink )
393 {
394 step = addedRevisedIds.size() > 0 ? 100.0 / addedRevisedIds.size() : 0;
395 it = mRevised->getFeatures( QgsFeatureRequest().setFilterFids( addedRevisedIds ) );
396 current = 0;
397 while ( it.nextFeature( f ) )
398 {
399 if ( feedback->isCanceled() )
400 break;
401
402 // added feature
403 if ( !addedSink->addFeature( f, QgsFeatureSink::FastInsert ) )
404 throw QgsProcessingException( writeFeatureError( addedSink.get(), parameters, QStringLiteral( "ADDED" ) ) );
405
406 current++;
407 feedback->setProgress( 0.10 * current * step + 90 ); // takes about 10% of time
408 }
409 }
410 feedback->setProgress( 100 );
411
412 feedback->pushInfo( QObject::tr( "%n feature(s) unchanged", nullptr, unchangedOriginalIds.size() ) );
413 feedback->pushInfo( QObject::tr( "%n feature(s) added", nullptr, addedRevisedIds.size() ) );
414 feedback->pushInfo( QObject::tr( "%n feature(s) deleted", nullptr, deleted ) );
415
416 if ( unchangedSink )
417 unchangedSink->finalize();
418 if ( addedSink )
419 addedSink->finalize();
420 if ( deletedSink )
421 deletedSink->finalize();
422
423 QVariantMap outputs;
424 outputs.insert( QStringLiteral( "UNCHANGED" ), unchangedDestId );
425 outputs.insert( QStringLiteral( "ADDED" ), addedDestId );
426 outputs.insert( QStringLiteral( "DELETED" ), deletedDestId );
427 outputs.insert( QStringLiteral( "UNCHANGED_COUNT" ), static_cast<long long>( unchangedOriginalIds.size() ) );
428 outputs.insert( QStringLiteral( "ADDED_COUNT" ), static_cast<long long>( addedRevisedIds.size() ) );
429 outputs.insert( QStringLiteral( "DELETED_COUNT" ), static_cast<long long>( deleted ) );
430
431 return outputs;
432}
433
@ VectorAnyGeometry
Any vector layer with geometry.
@ NoGeometry
Geometry is not required. It may still be returned if e.g. required for a filter condition.
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
virtual bool dropMValue()=0
Drops any measure values which exist in the geometry.
virtual bool dropZValue()=0
Drops any z-dimensions which exist in the geometry.
A vector of attributes.
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.
This class 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 & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
QgsFeatureRequest & setDestinationCrs(const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context)
Sets the destination crs for feature's geometries.
@ 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:58
QgsAttributes attributes
Definition qgsfeature.h:67
QgsFeatureId id
Definition qgsfeature.h:66
QgsGeometry geometry
Definition qgsfeature.h:69
bool hasGeometry() const
Returns true if the feature has an associated geometry.
void setGeometry(const QgsGeometry &geometry)
Set the feature's geometry.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:61
A geometry is the spatial representation of a feature.
QgsAbstractGeometry * get()
Returns a modifiable (non-const) reference to the underlying abstract geometry primitive.
bool equals(const QgsGeometry &geometry) const
Test if this geometry is exactly equal to another geometry.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
bool isGeosEqual(const QgsGeometry &) const
Compares the geometry with another geometry using GEOS.
Contains information about the context in which a processing algorithm is executed.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
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.
static Qgis::GeometryType geometryType(Qgis::WkbType type)
Returns the geometry type for a WKB type, e.g., both MultiPolygon and CurvePolygon would have a Polyg...
static QString displayString(Qgis::WkbType type)
Returns a non-translated display string type for a WKB type, e.g., the geometry name used in WKT geom...
static QString geometryDisplayString(Qgis::GeometryType type)
Returns a display string for a geometry type.
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features