QGIS API Documentation  3.27.0-Master (e113457133)
qgsgeometrychecker.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * qgsgeometrychecker.cpp *
3  * ------------------- *
4  * copyright : (C) 2014 by Sandro Mani / Sourcepole AG *
5  * email : [email protected] *
6  ***************************************************************************/
7 
8 /***************************************************************************
9  * *
10  * This program is free software; you can redistribute it and/or modify *
11  * it under the terms of the GNU General Public License as published by *
12  * the Free Software Foundation; either version 2 of the License, or *
13  * (at your option) any later version. *
14  * *
15  ***************************************************************************/
16 
17 #include <QtConcurrentMap>
18 #include <QFutureWatcher>
19 #include <QMutex>
20 #include <QTimer>
21 #include <QTextStream>
22 
24 #include "qgsgeometrychecker.h"
25 #include "qgsgeometrycheck.h"
26 #include "qgsfeaturepool.h"
27 #include "qgsproject.h"
28 #include "qgsvectorlayer.h"
29 #include "qgsgeometrycheckerror.h"
30 
31 
32 
33 QgsGeometryChecker::QgsGeometryChecker( const QList<QgsGeometryCheck *> &checks, QgsGeometryCheckContext *context, const QMap<QString, QgsFeaturePool *> &featurePools )
34  : mChecks( checks )
35  , mContext( context )
36  , mFeaturePools( featurePools )
37 {
38  for ( auto it = featurePools.constBegin(); it != mFeaturePools.constEnd(); ++it )
39  {
40  if ( it.value()->layer() )
41  {
42  it.value()->layer()->setReadOnly( true );
43  // Enter update mode to defer ogr dataset repacking until the checker has finished
44  it.value()->layer()->dataProvider()->enterUpdateMode();
45  }
46  }
47 }
48 
50 {
51  qDeleteAll( mCheckErrors );
52  qDeleteAll( mChecks );
53  for ( auto it = mFeaturePools.constBegin(); it != mFeaturePools.constEnd(); ++it )
54  {
55  if ( it.value()->layer() )
56  {
57  it.value()->layer()->dataProvider()->leaveUpdateMode();
58  it.value()->layer()->setReadOnly( false );
59  }
60  delete it.value();
61  }
62  delete mContext;
63 }
64 
65 QFuture<void> QgsGeometryChecker::execute( int *totalSteps )
66 {
67  if ( totalSteps )
68  {
69  *totalSteps = 0;
70  for ( QgsGeometryCheck *check : std::as_const( mChecks ) )
71  {
72  for ( auto it = mFeaturePools.constBegin(); it != mFeaturePools.constEnd(); ++it )
73  {
74  if ( check->checkType() <= QgsGeometryCheck::FeatureCheck )
75  {
76  *totalSteps += check->isCompatible( it.value()->layer() ) ? it.value()->allFeatureIds().size() : 0;
77  }
78  else
79  {
80  *totalSteps += 1;
81  }
82  }
83  }
84  }
85  QTimer *timer = new QTimer();
86  connect( timer, &QTimer::timeout, this, &QgsGeometryChecker::emitProgressValue );
87  QFutureWatcher<void> *watcher = new QFutureWatcher<void>();
88  connect( watcher, &QFutureWatcherBase::finished, timer, &QObject::deleteLater );
89  connect( watcher, &QFutureWatcherBase::finished, watcher, &QObject::deleteLater );
90 
91  QFuture<void> future = QtConcurrent::map( mChecks, RunCheckWrapper( this ) );
92  watcher->setFuture( future );
93  timer->start( 500 );
94 
95  return future;
96 }
97 
98 void QgsGeometryChecker::emitProgressValue()
99 {
100  emit progressValue( mFeedback.progress() );
101 }
102 
103 bool QgsGeometryChecker::fixError( QgsGeometryCheckError *error, int method, bool triggerRepaint )
104 {
105  mMessages.clear();
106  if ( error->status() >= QgsGeometryCheckError::StatusFixed )
107  {
108  return true;
109  }
110 #if 0
111  QTextStream( stdout ) << "Fixing " << error->description() << ": " << error->layerId() << ":" << error->featureId() << " @[" << error->vidx().part << ", " << error->vidx().ring << ", " << error->vidx().vertex << "](" << error->location().x() << ", " << error->location().y() << ") = " << error->value().toString() << endl;
112 #endif
113 
115  QgsRectangle recheckArea = error->affectedAreaBBox();
116 
117  error->check()->fixError( mFeaturePools, error, method, mMergeAttributeIndices, changes );
118 #if 0
119  QTextStream( stdout ) << " * Status: " << error->resolutionMessage() << endl;
120  static QVector<QString> strChangeWhat = { "ChangeFeature", "ChangePart", "ChangeRing", "ChangeNode" };
121  static QVector<QString> strChangeType = { "ChangeAdded", "ChangeRemoved", "ChangeChanged" };
122  for ( const QString &layerId : changes.keys() )
123  {
124  for ( const QgsFeatureId &fid : changes[layerId].keys() )
125  {
126  for ( const QgsGeometryCheck::Change &change : changes[layerId][fid] )
127  {
128  QTextStream( stdout ) << " * Change: " << layerId << ":" << fid << " :: " << strChangeWhat[change.what] << ":" << strChangeType[change.type] << ":(" << change.vidx.part << "," << change.vidx.ring << "," << change.vidx.vertex << ")" << endl;
129  }
130  }
131  }
132 #endif
133  emit errorUpdated( error, true );
134  if ( error->status() != QgsGeometryCheckError::StatusFixed )
135  {
136  return false;
137  }
138 
139  // If nothing was changed, stop here
140  if ( changes.isEmpty() )
141  {
142  return true;
143  }
144 
145  // Determine what to recheck
146  // - Collect all features which were changed, get affected area
147  QMap<QString, QSet<QgsFeatureId>> recheckFeatures;
148  for ( auto it = changes.constBegin(); it != changes.constEnd(); ++it )
149  {
150  const QMap<QgsFeatureId, QList<QgsGeometryCheck::Change>> &layerChanges = it.value();
151  QgsFeaturePool *featurePool = mFeaturePools[it.key()];
152  QgsCoordinateTransform t( featurePool->layer()->crs(), mContext->mapCrs, QgsProject::instance() );
154  for ( auto layerChangeIt = layerChanges.constBegin(); layerChangeIt != layerChanges.constEnd(); ++layerChangeIt )
155  {
156  bool removed = false;
157  for ( const QgsGeometryCheck::Change &change : layerChangeIt.value() )
158  {
159  if ( change.what == QgsGeometryCheck::ChangeFeature && change.type == QgsGeometryCheck::ChangeRemoved )
160  {
161  removed = true;
162  break;
163  }
164  }
165  if ( !removed )
166  {
167  QgsFeature f;
168  if ( featurePool->getFeature( layerChangeIt.key(), f ) )
169  {
170  recheckFeatures[it.key()].insert( layerChangeIt.key() );
172  }
173  }
174  }
175  }
176  // - Determine extent to recheck for gaps
177  for ( QgsGeometryCheckError *err : std::as_const( mCheckErrors ) )
178  {
179  if ( err->check()->checkType() == QgsGeometryCheck::LayerCheck )
180  {
181  if ( err->affectedAreaBBox().intersects( recheckArea ) )
182  {
183  recheckArea.combineExtentWith( err->affectedAreaBBox() );
184  }
185  }
186  }
187  recheckArea.grow( 10 * mContext->tolerance );
188  QMap<QString, QgsFeatureIds> recheckAreaFeatures;
189  for ( const QString &layerId : mFeaturePools.keys() )
190  {
191  QgsFeaturePool *featurePool = mFeaturePools[layerId];
192  QgsCoordinateTransform t( mContext->mapCrs, featurePool->layer()->crs(), QgsProject::instance() );
193  recheckAreaFeatures[layerId] = featurePool->getIntersects( t.transform( recheckArea ) );
194  }
195 
196  // Recheck feature / changed area to detect new errors
197  QList<QgsGeometryCheckError *> recheckErrors;
198  for ( const QgsGeometryCheck *check : std::as_const( mChecks ) )
199  {
200  if ( check->checkType() == QgsGeometryCheck::LayerCheck )
201  {
202  if ( !recheckAreaFeatures.isEmpty() )
203  {
204  check->collectErrors( mFeaturePools, recheckErrors, mMessages, nullptr, recheckAreaFeatures );
205  }
206  }
207  else
208  {
209  if ( !recheckFeatures.isEmpty() )
210  {
211  check->collectErrors( mFeaturePools, recheckErrors, mMessages, nullptr, recheckFeatures );
212  }
213  }
214  }
215 
216  // Go through error list, update other errors of the checked feature
217  for ( QgsGeometryCheckError *err : std::as_const( mCheckErrors ) )
218  {
219  if ( err == error || err->status() == QgsGeometryCheckError::StatusObsolete )
220  {
221  continue;
222  }
223 
224  QgsGeometryCheckError::Status oldStatus = err->status();
225 
226  bool handled = err->handleChanges( changes );
227 
228  // Check if this error now matches one found when rechecking the feature/area
229  QgsGeometryCheckError *matchErr = nullptr;
230  int nMatch = 0;
231  for ( QgsGeometryCheckError *recheckErr : std::as_const( recheckErrors ) )
232  {
233  if ( recheckErr->isEqual( err ) || recheckErr->closeMatch( err ) )
234  {
235  ++nMatch;
236  matchErr = recheckErr;
237  }
238  }
239  // If just one close match was found, take it
240  if ( nMatch == 1 && matchErr )
241  {
242  err->update( matchErr );
243  emit errorUpdated( err, err->status() != oldStatus );
244  recheckErrors.removeAll( matchErr );
245  delete matchErr;
246  continue;
247  }
248 
249  // If no match is found and the error is not fixed or obsolete, set it to obsolete if...
250  if ( err->status() < QgsGeometryCheckError::StatusFixed &&
251  (
252  // changes weren't handled
253  !handled ||
254  // or if it is a FeatureNodeCheck or FeatureCheck error whose feature was rechecked
255  ( err->check()->checkType() <= QgsGeometryCheck::FeatureCheck && recheckFeatures[err->layerId()].contains( err->featureId() ) ) ||
256  // or if it is a LayerCheck error within the rechecked area
257  ( err->check()->checkType() == QgsGeometryCheck::LayerCheck && recheckArea.contains( err->affectedAreaBBox() ) )
258  )
259  )
260  {
261  err->setObsolete();
262  emit errorUpdated( err, err->status() != oldStatus );
263  }
264  }
265 
266  // Add new errors
267  for ( QgsGeometryCheckError *recheckErr : std::as_const( recheckErrors ) )
268  {
269  emit errorAdded( recheckErr );
270  mCheckErrors.append( recheckErr );
271  }
272 
273  if ( triggerRepaint )
274  {
275  for ( const QString &layerId : changes.keys() )
276  {
277  mFeaturePools[layerId]->layer()->triggerRepaint();
278  }
279  }
280 
281  return true;
282 }
283 
284 void QgsGeometryChecker::runCheck( const QMap<QString, QgsFeaturePool *> &featurePools, const QgsGeometryCheck *check )
285 {
286  // Run checks
287  QList<QgsGeometryCheckError *> errors;
288  QStringList messages;
289  check->collectErrors( featurePools, errors, messages, &mFeedback );
290  mErrorListMutex.lock();
291  mCheckErrors.append( errors );
292  mMessages.append( messages );
293  mErrorListMutex.unlock();
294  for ( QgsGeometryCheckError *error : std::as_const( errors ) )
295  {
296  emit errorAdded( error );
297  }
298 }
299 
300 QgsGeometryChecker::RunCheckWrapper::RunCheckWrapper( QgsGeometryChecker *instance )
301  : mInstance( instance )
302 {
303 }
304 
305 void QgsGeometryChecker::RunCheckWrapper::operator()( const QgsGeometryCheck *check )
306 {
307  mInstance->runCheck( mInstance->mFeaturePools, check );
308 }
Class for doing transforms between two map coordinate systems.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const SIP_THROW(QgsCsException)
Transforms a rectangle from the source CRS to the destination CRS.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const SIP_THROW(QgsCsException)
Transform the point from the source CRS to the destination CRS.
A feature pool is based on a vector layer and caches features.
QgsFeatureIds getIntersects(const QgsRectangle &rect) const
Gets all feature ids in the bounding box rect.
QgsVectorLayer * layer() const
Gets a pointer to the underlying layer.
bool getFeature(QgsFeatureId id, QgsFeature &feature)
Retrieves the feature with the specified id into feature.
The feature class encapsulates a single feature including its unique ID, geometry and a list of field...
Definition: qgsfeature.h:56
QgsGeometry geometry
Definition: qgsfeature.h:67
double progress() const SIP_HOLDGIL
Returns the current progress reported by the feedback object.
Definition: qgsfeedback.h:80
Base configuration for geometry checks.
const QgsCoordinateReferenceSystem mapCrs
The coordinate system in which calculations should be done.
const double tolerance
The tolerance to allow for in geometry checks.
This represents an error reported by a geometry check.
const QString & layerId() const
The id of the layer on which this error has been detected.
Status
The status of an error.
@ StatusFixed
The error is fixed.
@ StatusObsolete
The error is obsolete because of other modifications.
Status status() const
The status of the error.
const QgsVertexId & vidx() const
The id of the affected vertex.
QString resolutionMessage() const
A message with details, how the error has been resolved.
QgsFeatureId featureId() const
The id of the feature on which this error has been detected.
virtual void update(const QgsGeometryCheckError *other)
Update this error with the information from other.
virtual QString description() const
The error description.
const QgsPointXY & location() const
The location of the error in map units.
const QgsGeometryCheck * check() const
The geometry check that created this error.
QVariant value() const
An additional value for the error.
void setObsolete()
Set the error status to obsolete.
virtual QgsRectangle affectedAreaBBox() const
The bounding box of the affected area of the error.
This class implements a geometry check.
QMap< QString, QMap< QgsFeatureId, QList< QgsGeometryCheck::Change > > > Changes
A collection of changes.
virtual void collectErrors(const QMap< QString, QgsFeaturePool * > &featurePools, QList< QgsGeometryCheckError * > &errors, QStringList &messages, QgsFeedback *feedback, const LayerFeatureIds &ids=QgsGeometryCheck::LayerFeatureIds()) const =0
The main worker method.
@ ChangeFeature
This change happens on feature level.
@ LayerCheck
The check controls a whole layer (topology checks)
@ FeatureCheck
The check controls geometries as a whole.
@ ChangeRemoved
Something has been removed.
virtual void fixError(const QMap< QString, QgsFeaturePool * > &featurePools, QgsGeometryCheckError *error, int method, const QMap< QString, int > &mergeAttributeIndices, Changes &changes) const
Fixes the error error with the specified method.
Manages and runs a set of geometry checks.
QFuture< void > execute(int *totalSteps=nullptr)
void progressValue(int value)
bool fixError(QgsGeometryCheckError *error, int method, bool triggerRepaint=false)
QgsGeometryChecker(const QList< QgsGeometryCheck * > &checks, QgsGeometryCheckContext *context, const QMap< QString, QgsFeaturePool * > &featurePools)
void errorAdded(QgsGeometryCheckError *error)
void errorUpdated(QgsGeometryCheckError *error, bool statusChanged)
const QMap< QString, QgsFeaturePool * > featurePools() const
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:79
double y
Definition: qgspointxy.h:63
Q_GADGET double x
Definition: qgspointxy.h:62
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:479
A rectangle specified with double values.
Definition: qgsrectangle.h:42
void grow(double delta)
Grows the rectangle in place by the specified amount.
Definition: qgsrectangle.h:296
void combineExtentWith(const QgsRectangle &rect)
Expands the rectangle so that it covers both the original rectangle and the given rectangle.
Definition: qgsrectangle.h:391
bool contains(const QgsRectangle &rect) const SIP_HOLDGIL
Returns true when rectangle contains other rectangle.
Definition: qgsrectangle.h:363
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
Definition: qgsfeatureid.h:28
Descripts a change to fix a geometry.
int vertex
Vertex number.
Definition: qgsvertexid.h:95
int part
Part number.
Definition: qgsvertexid.h:89
int ring
Ring number.
Definition: qgsvertexid.h:92