24QString QgsDetectVectorChangesAlgorithm::name()
 const 
   26  return QStringLiteral( 
"detectvectorchanges" );
 
   29QString QgsDetectVectorChangesAlgorithm::displayName()
 const 
   31  return QObject::tr( 
"Detect dataset changes" );
 
   34QStringList QgsDetectVectorChangesAlgorithm::tags()
 const 
   36  return QObject::tr( 
"added,dropped,new,deleted,features,geometries,difference,delta,revised,original,version" ).split( 
',' );
 
   39QString QgsDetectVectorChangesAlgorithm::group()
 const 
   41  return QObject::tr( 
"Vector general" );
 
   44QString QgsDetectVectorChangesAlgorithm::groupId()
 const 
   46  return QStringLiteral( 
"vectorgeneral" );
 
   49void QgsDetectVectorChangesAlgorithm::initAlgorithm( 
const QVariantMap & )
 
   54  auto 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() );
 
   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 );
 
   60  addParameter( matchTypeParam.release() );
 
   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" ) ) );
 
   71QString QgsDetectVectorChangesAlgorithm::shortHelpString()
 const 
   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." );
 
   92QString QgsDetectVectorChangesAlgorithm::shortDescription()
 const 
   94  return QObject::tr( 
"Calculates features which are unchanged, added or deleted between two dataset versions." );
 
   97QgsDetectVectorChangesAlgorithm *QgsDetectVectorChangesAlgorithm::createInstance()
 const 
   99  return new QgsDetectVectorChangesAlgorithm();
 
  104  mOriginal.reset( parameterAsSource( parameters, QStringLiteral( 
"ORIGINAL" ), context ) );
 
  108  mRevised.reset( parameterAsSource( parameters, QStringLiteral( 
"REVISED" ), context ) );
 
  112  mMatchType = 
static_cast<GeometryMatchType
>( parameterAsEnum( parameters, QStringLiteral( 
"MATCH_TYPE" ), context ) );
 
  114  switch ( mMatchType )
 
  117      if ( mOriginal->wkbType() != mRevised->wkbType() )
 
  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 );
 
  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 )
 
  137    const int originalIndex = mOriginal->fields().lookupField( field );
 
  138    mOriginalFieldsToCompareIndices.append( originalIndex );
 
  139    if ( originalIndex < 0 )
 
  140      missingOriginalFields << field;
 
  142    const int revisedIndex = mRevised->fields().lookupField( field );
 
  143    if ( revisedIndex < 0 )
 
  144      missingRevisedFields << field;
 
  145    mRevisedFieldsToCompareIndices.append( revisedIndex );
 
  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( 
',' ) ) );
 
  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() )
 
  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() )
 
  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() )
 
  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;
 
  187  attrs.resize( mFieldsToCompare.size() );
 
  195      originalGeometries.insert( f.
id(), f.
geometry() );
 
  198    if ( !mFieldsToCompare.empty() )
 
  201      for ( 
const int field : mOriginalFieldsToCompareIndices )
 
  205      originalAttributes.insert( f.
id(), attrs );
 
  210      if ( originalNullGeometryAttributes.contains( attrs ) )
 
  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)" )
 
  215                                 .arg( originalNullGeometryAttributes.value( attrs ) ) );
 
  219        originalNullGeometryAttributes.insert( attrs, f.
id() );
 
  229  QSet<QgsFeatureId> unchangedOriginalIds;
 
  230  QSet<QgsFeatureId> addedRevisedIds;
 
  235  step = mRevised->featureCount() > 0 ? 100.0 / mRevised->featureCount() : 0;
 
  238  it = mRevised->getFeatures( revisedRequest );
 
  246    for ( 
const int field : mRevisedFieldsToCompareIndices )
 
  248      attrs[idx++] = revisedFeature.
attributes().at( field );
 
  251    bool matched = 
false;
 
  255      if ( originalNullGeometryAttributes.contains( attrs ) )
 
  258        unchangedOriginalIds.insert( originalNullGeometryAttributes.value( attrs ) );
 
  265      const QList<QgsFeatureId> candidates = index.intersects( revisedFeature.
geometry().
boundingBox() );
 
  272        if ( unchangedOriginalIds.contains( candidateId ) )
 
  279        if ( !mFieldsToCompare.empty() )
 
  281          if ( attrs != originalAttributes[candidateId] )
 
  288        QgsGeometry original = originalGeometries.value( candidateId );
 
  292          revised = revisedFeature.
geometry();
 
  294          switch ( mMatchType )
 
  310        bool geometryMatch = 
false;
 
  311        switch ( mMatchType )
 
  320            geometryMatch = revised.
equals( original );
 
  327          unchangedOriginalIds.insert( candidateId );
 
  337      addedRevisedIds.insert( revisedFeature.
id() );
 
  341    feedback->
setProgress( 0.70 * current * step + 10 ); 
 
  347  step = mOriginal->featureCount() > 0 ? 100.0 / mOriginal->featureCount() : 0;
 
  350  it = mOriginal->getFeatures( request );
 
  362    if ( unchangedOriginalIds.contains( f.
id() ) )
 
  368          throw QgsProcessingException( writeFeatureError( unchangedSink.get(), parameters, QStringLiteral( 
"UNCHANGED" ) ) );
 
  377          throw QgsProcessingException( writeFeatureError( deletedSink.get(), parameters, QStringLiteral( 
"DELETED" ) ) );
 
  383    feedback->
setProgress( 0.10 * current * step + 80 ); 
 
  394    step = addedRevisedIds.size() > 0 ? 100.0 / addedRevisedIds.size() : 0;
 
  395    it = mRevised->getFeatures( 
QgsFeatureRequest().setFilterFids( addedRevisedIds ) );
 
  404        throw QgsProcessingException( writeFeatureError( addedSink.get(), parameters, QStringLiteral( 
"ADDED" ) ) );
 
  407      feedback->
setProgress( 0.10 * current * step + 90 ); 
 
  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 ) );
 
  417    unchangedSink->finalize();
 
  419    addedSink->finalize();
 
  421    deletedSink->finalize();
 
  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 ) );
 
@ 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.
 
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 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