26using namespace Qt::StringLiterals;
30QString QgsDetectVectorChangesAlgorithm::name()
const
32 return u
"detectvectorchanges"_s;
35QString QgsDetectVectorChangesAlgorithm::displayName()
const
37 return QObject::tr(
"Detect dataset changes" );
40QStringList QgsDetectVectorChangesAlgorithm::tags()
const
42 return QObject::tr(
"added,dropped,new,deleted,features,geometries,difference,delta,revised,original,version" ).split(
',' );
45QString QgsDetectVectorChangesAlgorithm::group()
const
47 return QObject::tr(
"Vector general" );
50QString QgsDetectVectorChangesAlgorithm::groupId()
const
52 return u
"vectorgeneral"_s;
55void QgsDetectVectorChangesAlgorithm::initAlgorithm(
const QVariantMap & )
60 auto compareAttributesParam = std::make_unique<
63 addParameter( compareAttributesParam.release() );
65 std::unique_ptr<QgsProcessingParameterDefinition> matchTypeParam = std::make_unique<
66 QgsProcessingParameterEnum>( u
"MATCH_TYPE"_s, QObject::tr(
"Geometry comparison behavior" ), QStringList() << QObject::tr(
"Exact Match" ) << QObject::tr(
"Tolerant Match (Topological Equality)" ),
false, 1 );
68 addParameter( matchTypeParam.release() );
75 addOutput(
new QgsProcessingOutputNumber( u
"ADDED_COUNT"_s, QObject::tr(
"Count of features added in revised layer" ) ) );
76 addOutput(
new QgsProcessingOutputNumber( u
"DELETED_COUNT"_s, QObject::tr(
"Count of features deleted from original layer" ) ) );
79QString QgsDetectVectorChangesAlgorithm::shortHelpString()
const
82 "This algorithm compares two vector layers, and determines which features are unchanged, added or deleted between "
83 "the two. It is designed for comparing two different versions of the same dataset.\n\n"
84 "When comparing features, the original and revised feature geometries will be compared against each other. Depending "
85 "on the Geometry Comparison Behavior setting, the comparison will either be made using an exact comparison (where "
86 "geometries must be an exact match for each other, including the order and count of vertices) or a topological "
87 "comparison only (where geometries are considered equal if all of their component edges overlap. E.g. "
88 "lines with the same vertex locations but opposite direction will be considered equal by this method). If the topological "
89 "comparison is selected then any z or m values present in the geometries will not be compared.\n\n"
90 "By default, the algorithm compares all attributes from the original and revised features. If the Attributes to Consider for Match "
91 "parameter is changed, then only the selected attributes will be compared (e.g. allowing users to ignore a timestamp or ID field "
92 "which is expected to change between the revisions).\n\n"
93 "If any features in the original or revised layers do not have an associated geometry, then care must be taken to ensure "
94 "that these features have a unique set of attributes selected for comparison. If this condition is not met, warnings will be "
95 "raised and the resultant outputs may be misleading.\n\n"
96 "The algorithm outputs three layers, one containing all features which are considered to be unchanged between the revisions, "
97 "one containing features deleted from the original layer which are not present in the revised layer, and one containing features "
98 "added to the revised layer which are not present in the original layer."
102QString QgsDetectVectorChangesAlgorithm::shortDescription()
const
104 return QObject::tr(
"Calculates features which are unchanged, added or deleted between two dataset versions." );
107QgsDetectVectorChangesAlgorithm *QgsDetectVectorChangesAlgorithm::createInstance()
const
109 return new QgsDetectVectorChangesAlgorithm();
114 mOriginal.reset( parameterAsSource( parameters, u
"ORIGINAL"_s, context ) );
118 mRevised.reset( parameterAsSource( parameters, u
"REVISED"_s, context ) );
122 mMatchType =
static_cast<GeometryMatchType
>( parameterAsEnum( parameters, u
"MATCH_TYPE"_s, context ) );
124 switch ( mMatchType )
127 if ( mOriginal->wkbType() != mRevised->wkbType() )
129 QObject::tr(
"Geometry type of revised layer (%1) does not match the original layer (%2). Consider using the \"Tolerant Match\" option instead." )
137 QObject::tr(
"Geometry type of revised layer (%1) does not match the original layer (%2)" )
143 if ( mOriginal->sourceCrs() != mRevised->sourceCrs() )
145 QObject::tr(
"CRS for revised layer (%1) does not match the original layer (%2) - reprojection accuracy may affect geometry matching" )
146 .arg( mOriginal->sourceCrs().userFriendlyIdentifier(), mRevised->sourceCrs().userFriendlyIdentifier() ),
150 mFieldsToCompare = parameterAsStrings( parameters, u
"COMPARE_ATTRIBUTES"_s, context );
151 mOriginalFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
152 mRevisedFieldsToCompareIndices.reserve( mFieldsToCompare.size() );
153 QStringList missingOriginalFields;
154 QStringList missingRevisedFields;
155 for (
const QString &field : mFieldsToCompare )
157 const int originalIndex = mOriginal->fields().lookupField( field );
158 mOriginalFieldsToCompareIndices.append( originalIndex );
159 if ( originalIndex < 0 )
160 missingOriginalFields << field;
162 const int revisedIndex = mRevised->fields().lookupField( field );
163 if ( revisedIndex < 0 )
164 missingRevisedFields << field;
165 mRevisedFieldsToCompareIndices.append( revisedIndex );
168 if ( !missingOriginalFields.empty() )
169 throw QgsProcessingException( QObject::tr(
"Original layer missing selected comparison attributes: %1" ).arg( missingOriginalFields.join(
',' ) ) );
170 if ( !missingRevisedFields.empty() )
171 throw QgsProcessingException( QObject::tr(
"Revised layer missing selected comparison attributes: %1" ).arg( missingRevisedFields.join(
',' ) ) );
178 QString unchangedDestId;
179 std::unique_ptr<QgsFeatureSink> unchangedSink( parameterAsSink( parameters, u
"UNCHANGED"_s, context, unchangedDestId, mOriginal->fields(), mOriginal->wkbType(), mOriginal->sourceCrs() ) );
180 if ( !unchangedSink && parameters.value( u
"UNCHANGED"_s ).isValid() )
184 std::unique_ptr<QgsFeatureSink> addedSink( parameterAsSink( parameters, u
"ADDED"_s, context, addedDestId, mRevised->fields(), mRevised->wkbType(), mRevised->sourceCrs() ) );
185 if ( !addedSink && parameters.value( u
"ADDED"_s ).isValid() )
188 QString deletedDestId;
189 std::unique_ptr<QgsFeatureSink> deletedSink( parameterAsSink( parameters, u
"DELETED"_s, context, deletedDestId, mOriginal->fields(), mOriginal->wkbType(), mOriginal->sourceCrs() ) );
190 if ( !deletedSink && parameters.value( u
"DELETED"_s ).isValid() )
200 double step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
201 QHash<QgsFeatureId, QgsGeometry> originalGeometries;
202 QHash<QgsFeatureId, QgsAttributes> originalAttributes;
203 QHash<QgsAttributes, QgsFeatureId> originalNullGeometryAttributes;
204 QHash<QgsAttributes, QgsFeatureId> originalEmptyGeometryAttributes;
208 attrs.resize( mFieldsToCompare.size() );
214 if ( !mFieldsToCompare.empty() )
217 for (
const int field : mOriginalFieldsToCompareIndices )
221 originalAttributes.insert( f.
id(), attrs );
226 originalGeometries.insert( f.
id(), f.
geometry() );
230 auto emptyGeomIt = originalEmptyGeometryAttributes.constFind( attrs );
231 if ( emptyGeomIt != originalEmptyGeometryAttributes.constEnd() )
235 "A non-unique set of comparison attributes was found for "
236 "one or more features with EMPTY geometries - results may be misleading (features %1 and %2)"
239 .arg( emptyGeomIt.value() )
244 originalEmptyGeometryAttributes.insert( attrs, f.
id() );
249 if ( originalNullGeometryAttributes.contains( attrs ) )
253 "A non-unique set of comparison attributes was found for "
254 "one or more features without geometries - results may be misleading (features %1 and %2)"
257 .arg( originalNullGeometryAttributes.value( attrs ) )
262 originalNullGeometryAttributes.insert( attrs, f.
id() );
272 QSet<QgsFeatureId> unchangedOriginalIds;
273 QSet<QgsFeatureId> addedRevisedIds;
278 step = mRevised->featureCount() > 0 ? 100.0 / mRevised->featureCount() : 0;
281 it = mRevised->getFeatures( revisedRequest );
289 for (
const int field : mRevisedFieldsToCompareIndices )
291 attrs[idx++] = revisedFeature.
attributes().at( field );
294 bool matched =
false;
298 if ( originalNullGeometryAttributes.contains( attrs ) )
301 unchangedOriginalIds.insert( originalNullGeometryAttributes.value( attrs ) );
307 auto emptyIt = originalEmptyGeometryAttributes.constFind( attrs );
308 if ( emptyIt != originalEmptyGeometryAttributes.constEnd() )
311 unchangedOriginalIds.insert( emptyIt.value() );
318 const QList<QgsFeatureId> candidates = index.intersects( revisedFeature.
geometry().
boundingBox() );
325 if ( unchangedOriginalIds.contains( candidateId ) )
332 if ( !mFieldsToCompare.empty() )
334 if ( attrs != originalAttributes[candidateId] )
341 QgsGeometry original = originalGeometries.value( candidateId );
345 revised = revisedFeature.
geometry();
347 switch ( mMatchType )
363 bool geometryMatch =
false;
364 switch ( mMatchType )
380 unchangedOriginalIds.insert( candidateId );
390 addedRevisedIds.insert( revisedFeature.
id() );
394 feedback->
setProgress( 0.70 * current * step + 10 );
400 step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
403 it = mOriginal->getFeatures( request );
408 QList<QgsFeatureId> emptyGeometryIds = originalEmptyGeometryAttributes.values();
417 if ( g.
isNull() && emptyGeometryIds.contains( f.
id() ) )
423 if ( unchangedOriginalIds.contains( f.
id() ) )
448 feedback->
setProgress( 0.10 * current * step + 80 );
459 step = addedRevisedIds.size() > 0 ? 100.0 / addedRevisedIds.size() : 0;
460 it = mRevised->getFeatures(
QgsFeatureRequest().setFilterFids( addedRevisedIds ) );
474 feedback->
setProgress( 0.10 * current * step + 90 );
479 feedback->
pushInfo( QObject::tr(
"%n feature(s) unchanged",
nullptr, unchangedOriginalIds.size() ) );
480 feedback->
pushInfo( QObject::tr(
"%n feature(s) added",
nullptr, addedRevisedIds.size() ) );
481 feedback->
pushInfo( QObject::tr(
"%n feature(s) deleted",
nullptr, deleted ) );
485 unchangedSink->finalize();
490 addedSink->finalize();
495 deletedSink->finalize();
500 outputs.insert( u
"UNCHANGED"_s, unchangedDestId );
501 outputs.insert( u
"ADDED"_s, addedDestId );
502 outputs.insert( u
"DELETED"_s, deletedDestId );
503 outputs.insert( u
"UNCHANGED_COUNT"_s,
static_cast<long long>( unchangedOriginalIds.size() ) );
504 outputs.insert( u
"ADDED_COUNT"_s,
static_cast<long long>( addedRevisedIds.size() ) );
505 outputs.insert( u
"DELETED_COUNT"_s,
static_cast<long long>( deleted ) );
@ 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.
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 & 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...
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.
void setProgress(double progress)
Sets the current progress for the feedback object.
static std::unique_ptr< QgsAbstractGeometry > geomFromWkbType(Qgis::WkbType t)
Returns empty geometry from wkb type.
A geometry is the spatial representation of a feature.
bool isExactlyEqual(const QgsGeometry &geometry, Qgis::GeometryBackend backend=Qgis::GeometryBackend::QGIS) const
Compares the geometry with another geometry using the specified backend.
QgsAbstractGeometry * get()
Returns a modifiable (non-const) reference to the underlying abstract geometry primitive.
bool isEmpty() const
Returns true if the geometry is empty (eg a linestring with no vertices, or a collection with no geom...
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.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context.
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.
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
A numeric output for processing algorithms.
An enum based parameter for processing algorithms, allowing for selection from predefined values.
A feature sink output for processing algorithms.
An input feature source (such as vector layers) parameter for processing algorithms.
A vector layer or feature source field parameter for processing algorithms.
void setDefaultToAllFields(bool enabled)
Sets whether a parameter which allows multiple selections (see allowMultiple()) should automatically ...
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 Q_INVOKABLE 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 Q_INVOKABLE 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