QGIS API Documentation 4.3.0-Master (d3b565c628d)
Loading...
Searching...
No Matches
qgsalgorithmkmeansclustering.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmkmeansclustering.cpp
3 ---------------------
4 begin : June 2018
5 copyright : (C) 2018 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 <random>
21#include <unordered_map>
22
24
25#include <QString>
26
27using namespace Qt::StringLiterals;
28
30
31constexpr uint KMEANS_MAX_ITERATIONS = 1000;
32
33QString QgsKMeansClusteringAlgorithm::name() const
34{
35 return u"kmeansclustering"_s;
36}
37
38QString QgsKMeansClusteringAlgorithm::displayName() const
39{
40 return QObject::tr( "K-means clustering" );
41}
42
43QStringList QgsKMeansClusteringAlgorithm::tags() const
44{
45 return QObject::tr( "clustering,clusters,kmeans,points" ).split( ',' );
46}
47
48QString QgsKMeansClusteringAlgorithm::group() const
49{
50 return QObject::tr( "Vector analysis" );
51}
52
53QString QgsKMeansClusteringAlgorithm::groupId() const
54{
55 return u"vectoranalysis"_s;
56}
57
58void QgsKMeansClusteringAlgorithm::initAlgorithm( const QVariantMap & )
59{
60 addParameter( new QgsProcessingParameterFeatureSource( u"INPUT"_s, QObject::tr( "Input layer" ), QList<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorAnyGeometry ) ) );
61 addParameter( new QgsProcessingParameterNumber( u"CLUSTERS"_s, QObject::tr( "Number of clusters" ), Qgis::ProcessingNumberParameterType::Integer, 5, false, 1 ) );
62
63 QStringList initializationMethods;
64 initializationMethods << QObject::tr( "Farthest points" ) << QObject::tr( "K-means++" );
65 addParameter( new QgsProcessingParameterEnum( u"METHOD"_s, QObject::tr( "Method" ), initializationMethods, false, 0, false ) );
66
67 auto fieldNameParam = std::make_unique<QgsProcessingParameterString>( u"FIELD_NAME"_s, QObject::tr( "Cluster field name" ), u"CLUSTER_ID"_s );
68 fieldNameParam->setFlags( fieldNameParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
69 addParameter( fieldNameParam.release() );
70 auto sizeFieldNameParam = std::make_unique<QgsProcessingParameterString>( u"SIZE_FIELD_NAME"_s, QObject::tr( "Cluster size field name" ), u"CLUSTER_SIZE"_s );
71 sizeFieldNameParam->setFlags( sizeFieldNameParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
72 addParameter( sizeFieldNameParam.release() );
73
74 addParameter( new QgsProcessingParameterFeatureSink( u"OUTPUT"_s, QObject::tr( "Clusters" ), Qgis::ProcessingSourceType::VectorAnyGeometry ) );
75}
76
77QString QgsKMeansClusteringAlgorithm::shortHelpString() const
78{
79 return QObject::tr(
80 "This algorithm calculates the 2D distance based k-means cluster number for each input feature.\n\n"
81 "If input geometries are lines or polygons, the clustering is based on the centroid of the feature."
82 );
83}
84
85QList<QgsAcademicReference> QgsKMeansClusteringAlgorithm::academicReferences() const
86{
88 createPresentation( { u"Arthur, D."_s, u"Vassilvitskii, S."_s }, 2007, u"K-Means++: The Advantages of Careful Seeding"_s, u"Proc. of the Annu. ACM-SIAM Symp. on Discrete Algorithms"_s, QString(), u"8"_s );
89
91 { u"Bhattacharya, A."_s, u"Eube, J."_s, u"Röglin, H."_s, u"Schmidt, M."_s },
92 2019,
93 u"Noisy, Greedy and Not So Greedy k-means++"_s,
94 u"28th Annual European Symposium on Algorithms (ESA 2020)"_s,
95 u"Leibniz International Proceedings in Informatics (LIPIcs)"_s,
96 u"pp. 18:1-18:21"_s
97 );
98
99 return { ref1, ref2 };
100}
101
102QString QgsKMeansClusteringAlgorithm::shortDescription() const
103{
104 return QObject::tr( "Calculates the 2D distance based k-means cluster number for each input feature." );
105}
106
107QgsKMeansClusteringAlgorithm *QgsKMeansClusteringAlgorithm::createInstance() const
108{
109 return new QgsKMeansClusteringAlgorithm();
110}
111
112QVariantMap QgsKMeansClusteringAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
113{
114 QGS_MARK_ALGORITHM_SOURCE
115
116 std::unique_ptr<QgsProcessingFeatureSource> source( parameterAsSource( parameters, u"INPUT"_s, context ) );
117 if ( !source )
118 throw QgsProcessingException( invalidSourceError( parameters, u"INPUT"_s ) );
119
120 int k = parameterAsInt( parameters, u"CLUSTERS"_s, context );
121 int initializationMethod = parameterAsInt( parameters, u"METHOD"_s, context );
122
123 QgsFields outputFields = source->fields();
124 QgsFields newFields;
125 const QString clusterFieldName = parameterAsString( parameters, u"FIELD_NAME"_s, context );
126 newFields.append( QgsField( clusterFieldName, QMetaType::Type::Int ) );
127 const QString clusterSizeFieldName = parameterAsString( parameters, u"SIZE_FIELD_NAME"_s, context );
128 newFields.append( QgsField( clusterSizeFieldName, QMetaType::Type::Int ) );
129 outputFields = QgsProcessingUtils::combineFields( outputFields, newFields );
130
131 QString dest;
132 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u"OUTPUT"_s, context, dest, outputFields, source->wkbType(), source->sourceCrs() ) );
133 if ( !sink )
134 throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) );
135
136 // build list of point inputs - if it's already a point, use that. If not, take the centroid.
137 feedback->pushInfo( QObject::tr( "Collecting input points" ) );
138 const double step = source->featureCount() > 0 ? 50.0 / static_cast< double >( source->featureCount() ) : 1;
139 int i = 0;
140 int n = 0;
141 int featureWithGeometryCount = 0;
142 QgsFeature feat;
143
144 std::vector<Feature> clusterFeatures;
145 QgsFeatureIterator features = source->getFeatures( QgsFeatureRequest().setNoAttributes() );
146 QHash<QgsFeatureId, std::size_t> idToObj;
147 while ( features.nextFeature( feat ) )
148 {
149 i++;
150 if ( feedback->isCanceled() )
151 {
152 break;
153 }
154
155 feedback->setProgress( i * step );
156 if ( !feat.hasGeometry() )
157 continue;
158 featureWithGeometryCount++;
159
160 QgsPointXY point;
163 else
164 {
165 const QgsGeometry centroid = feat.geometry().centroid();
166 if ( centroid.isNull() )
167 continue; // centroid failed, e.g. empty linestring
168
170 }
171
172 n++;
173
174 idToObj[feat.id()] = clusterFeatures.size();
175 clusterFeatures.emplace_back( Feature( point ) );
176 }
177
178 if ( n < k )
179 {
180 feedback->reportError( QObject::tr( "Number of geometries is less than the number of clusters requested, not all clusters will get data" ) );
181 k = n;
182 }
183
184 if ( k > 1 )
185 {
186 feedback->pushInfo( QObject::tr( "Calculating clusters" ) );
187
188 // cluster centers
189 std::vector<QgsPointXY> centers( k );
190 switch ( initializationMethod )
191 {
192 case 0: // farthest points
193 initClustersFarthestPoints( clusterFeatures, centers, k, feedback );
194 break;
195 case 1: // k-means++
196 initClustersPlusPlus( clusterFeatures, centers, k, feedback );
197 break;
198 default:
199 break;
200 }
201 calculateKMeans( clusterFeatures, centers, k, feedback );
202 }
203
204 // cluster size
205 std::unordered_map<int, int> clusterSize;
206 for ( auto it = idToObj.constBegin(); it != idToObj.constEnd(); ++it )
207 {
208 clusterSize[clusterFeatures[it.value()].cluster]++;
209 }
210
211 features = source->getFeatures();
212 i = 0;
213 while ( features.nextFeature( feat ) )
214 {
215 i++;
216 if ( feedback->isCanceled() )
217 {
218 break;
219 }
220
221 feedback->setProgress( 50 + i * step );
222 QgsAttributes attr = feat.attributes();
223 const auto obj = idToObj.find( feat.id() );
224 if ( !feat.hasGeometry() || obj == idToObj.end() )
225 {
226 attr << QVariant() << QVariant();
227 }
228 else if ( k <= 1 )
229 {
230 attr << 0 << featureWithGeometryCount;
231 }
232 else
233 {
234 const int cluster = clusterFeatures[*obj].cluster;
235 attr << cluster << clusterSize[cluster];
236 }
237 feat.setAttributes( attr );
238 if ( !sink->addFeature( feat, QgsFeatureSink::FastInsert ) )
239 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
240 else
241 feedback->featureAddedToSink( u"OUTPUT"_s );
242 }
243
244 sink->finalize();
245 feedback->featureSinkFinalized( u"OUTPUT"_s );
246
247 QVariantMap outputs;
248 outputs.insert( u"OUTPUT"_s, dest );
249 return outputs;
250}
251
252// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c
253
254void QgsKMeansClusteringAlgorithm::initClustersFarthestPoints( std::vector<Feature> &points, std::vector<QgsPointXY> &centers, const int k, QgsProcessingFeedback *feedback )
255{
256 const std::size_t n = points.size();
257 if ( n == 0 )
258 return;
259
260 if ( n == 1 )
261 {
262 for ( int i = 0; i < k; i++ )
263 centers[i] = points[0].point;
264 return;
265 }
266
267 std::size_t duplicateCount = 1;
268 // initially scan for two most distance points from each other, p1 and p2
269 std::size_t p1 = 0;
270 std::size_t p2 = 0;
271 double distanceP1 = 0;
272 double distanceP2 = 0;
273 double maxDistance = -1;
274 for ( std::size_t i = 1; i < n; i++ )
275 {
276 distanceP1 = points[i].point.sqrDist( points[p1].point );
277 distanceP2 = points[i].point.sqrDist( points[p2].point );
278
279 // if this point is further then existing candidates, replace our choice
280 if ( ( distanceP1 > maxDistance ) || ( distanceP2 > maxDistance ) )
281 {
282 maxDistance = std::max( distanceP1, distanceP2 );
283 if ( distanceP1 > distanceP2 )
284 p2 = i;
285 else
286 p1 = i;
287 }
288
289 // also record count of duplicate points
290 if ( qgsDoubleNear( distanceP1, 0 ) || qgsDoubleNear( distanceP2, 0 ) )
291 duplicateCount++;
292 }
293
294 if ( feedback && duplicateCount > 1 )
295 {
296 feedback->pushWarning( QObject::tr( "There are at least %n duplicate input(s), the number of output clusters may be less than was requested", nullptr, static_cast< int >( duplicateCount ) ) );
297 }
298
299 // By now two points should be found and be not the same
300 // Q_ASSERT( p1 != p2 && maxDistance >= 0 );
301
302 // Accept these two points as our initial centers
303 centers[0] = points[p1].point;
304 centers[1] = points[p2].point;
305
306 if ( k > 2 )
307 {
308 // array of minimum distance to a point from accepted cluster centers
309 std::vector<double> distances( n );
310
311 // initialize array with distance to first object
312 for ( std::size_t j = 0; j < n; j++ )
313 {
314 distances[j] = points[j].point.sqrDist( centers[0] );
315 }
316 distances[p1] = -1;
317 distances[p2] = -1;
318
319 // loop i on clusters, skip 0 and 1 as found already
320 for ( int i = 2; i < k; i++ )
321 {
322 std::size_t candidateCenter = 0;
323 double maxDistance = std::numeric_limits<double>::lowest();
324
325 // loop j on points
326 for ( std::size_t j = 0; j < n; j++ )
327 {
328 // accepted clusters are already marked with distance = -1
329 if ( distances[j] < 0 )
330 continue;
331
332 // update minimal distance with previously accepted cluster
333 distances[j] = std::min( points[j].point.sqrDist( centers[i - 1] ), distances[j] );
334
335 // greedily take a point that's farthest from any of accepted clusters
336 if ( distances[j] > maxDistance )
337 {
338 candidateCenter = j;
339 maxDistance = distances[j];
340 }
341 }
342
343 // checked earlier by counting entries on input, just in case
344 Q_ASSERT( maxDistance >= 0 );
345
346 // accept candidate to centers
347 distances[candidateCenter] = -1;
348 // copy the point coordinates into the initial centers array
349 centers[i] = points[candidateCenter].point;
350 }
351 }
352}
353
354void QgsKMeansClusteringAlgorithm::initClustersPlusPlus( std::vector<Feature> &points, std::vector<QgsPointXY> &centers, const int k, QgsProcessingFeedback *feedback )
355{
356 const std::size_t n = points.size();
357 if ( n == 0 )
358 return;
359
360 if ( n == 1 )
361 {
362 for ( int i = 0; i < k; i++ )
363 centers[i] = points[0].point;
364 return;
365 }
366
367 // randomly select the first point
368 std::random_device rd;
369 std::mt19937 gen( rd() );
370 std::uniform_int_distribution<size_t> distrib( 0, n - 1 );
371
372 std::size_t p1 = distrib( gen );
373 centers[0] = points[p1].point;
374
375 // calculate distances and total error (sum of distances of points to center)
376 std::vector<double> distances( n );
377 double totalError = 0;
378 std::size_t duplicateCount = 1;
379 for ( size_t i = 0; i < n; i++ )
380 {
381 double distance = points[i].point.sqrDist( centers[0] );
382 distances[i] = distance;
383 totalError += distance;
384 if ( qgsDoubleNear( distance, 0 ) )
385 {
386 duplicateCount++;
387 }
388 }
389 if ( feedback && duplicateCount > 1 )
390 {
391 feedback->pushWarning( QObject::tr( "There are at least %n duplicate input(s), the number of output clusters may be less than was requested", nullptr, static_cast< int >( duplicateCount ) ) );
392 }
393
394 // greedy kmeans++
395 // test not only one center but L possible centers
396 // chosen independently according to the same probability distribution), and then among these L
397 // centers, the one that decreases the k-means cost the most is chosen
398 // Bhattacharya, Anup & Eube, Jan & Röglin, Heiko & Schmidt, Melanie. (2019). Noisy, greedy and Not So greedy k-means++
399 unsigned int numCandidateCenters = 2 + static_cast< int >( std::floor( std::log( k ) ) );
400 std::vector<double> randomNumbers( numCandidateCenters );
401 std::vector<size_t> candidateCenters( numCandidateCenters );
402
403 std::uniform_real_distribution<double> dis( 0.0, 1.0 );
404 for ( int i = 1; i < k; i++ )
405 {
406 // sampling with probability proportional to the squared distance to the closest existing center
407 for ( unsigned int j = 0; j < numCandidateCenters; j++ )
408 {
409 randomNumbers[j] = dis( gen ) * totalError;
410 }
411
412 // cumulative sum, keep distances for later use
413 std::vector<double> cumSum = distances;
414 for ( size_t j = 1; j < n; j++ )
415 {
416 cumSum[j] += cumSum[j - 1];
417 }
418
419 // binary search for the index of the first element greater than or equal to random numbers
420 for ( unsigned int j = 0; j < numCandidateCenters; j++ )
421 {
422 size_t low = 0;
423 size_t high = n - 1;
424
425 while ( low <= high )
426 {
427 size_t mid = low + ( high - low ) / 2;
428 if ( cumSum[mid] < randomNumbers[j] )
429 {
430 low = mid + 1;
431 }
432 else
433 {
434 // size_t cannot be negative
435 if ( mid == 0 )
436 break;
437
438 high = mid - 1;
439 }
440 }
441 // clip candidate center to the number of points
442 if ( low >= n )
443 {
444 low = n - 1;
445 }
446 candidateCenters[j] = low;
447 }
448
449 std::vector<std::vector<double>> distancesCandidateCenters( numCandidateCenters, std::vector<double>( n ) );
450 ;
451
452 // store distances between previous and new candidate center, error and best candidate index
453 double currentError = 0;
454 double lowestError = std::numeric_limits<double>::max();
455 unsigned int bestCandidateIndex = 0;
456 for ( unsigned int j = 0; j < numCandidateCenters; j++ )
457 {
458 for ( size_t z = 0; z < n; z++ )
459 {
460 // distance to candidate center
461 double distance = points[candidateCenters[j]].point.sqrDist( points[z].point );
462 // if distance to previous center is less than the current distance, use that
463 if ( distance > distances[z] )
464 {
465 distance = distances[z];
466 }
467 distancesCandidateCenters[j][z] = distance;
468 currentError += distance;
469 }
470 if ( lowestError > currentError )
471 {
472 lowestError = currentError;
473 bestCandidateIndex = j;
474 }
475 }
476
477 // update distances with the best candidate center values
478 for ( size_t j = 0; j < n; j++ )
479 {
480 distances[j] = distancesCandidateCenters[bestCandidateIndex][j];
481 }
482 // store the best candidate center
483 centers[i] = points[candidateCenters[bestCandidateIndex]].point;
484 // update error
485 totalError = lowestError;
486 }
487}
488
489// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c
490
491void QgsKMeansClusteringAlgorithm::calculateKMeans( std::vector<QgsKMeansClusteringAlgorithm::Feature> &objs, std::vector<QgsPointXY> &centers, int k, QgsProcessingFeedback *feedback )
492{
493 int converged = false;
494 bool changed = false;
495
496 // avoid reallocating weights array for every iteration
497 std::vector<uint> weights( k );
498
499 uint i = 0;
500 for ( i = 0; i < KMEANS_MAX_ITERATIONS && !converged; i++ )
501 {
502 if ( feedback && feedback->isCanceled() )
503 break;
504
505 findNearest( objs, centers, k, changed );
506 updateMeans( objs, centers, weights, k );
507 converged = !changed;
508 }
509
510 if ( !converged && feedback )
511 feedback->reportError( QObject::tr( "Clustering did not converge after %n iteration(s)", nullptr, static_cast<int>( i ) ) );
512 else if ( feedback )
513 feedback->pushInfo( QObject::tr( "Clustering converged after %n iteration(s)", nullptr, static_cast<int>( i ) ) );
514}
515
516// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c
517
518void QgsKMeansClusteringAlgorithm::findNearest( std::vector<QgsKMeansClusteringAlgorithm::Feature> &points, const std::vector<QgsPointXY> &centers, const int k, bool &changed )
519{
520 changed = false;
521 const std::size_t n = points.size();
522 for ( std::size_t i = 0; i < n; i++ )
523 {
524 Feature &point = points[i];
525
526 // Initialize with distance to first cluster
527 double currentDistance = point.point.sqrDist( centers[0] );
528 int currentCluster = 0;
529
530 // Check all other cluster centers and find the nearest
531 for ( int cluster = 1; cluster < k; cluster++ )
532 {
533 const double distance = point.point.sqrDist( centers[cluster] );
534 if ( distance < currentDistance )
535 {
536 currentDistance = distance;
537 currentCluster = cluster;
538 }
539 }
540
541 // Store the nearest cluster this object is in
542 if ( point.cluster != currentCluster )
543 {
544 changed = true;
545 point.cluster = currentCluster;
546 }
547 }
548}
549
550// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c
551
552void QgsKMeansClusteringAlgorithm::updateMeans( const std::vector<Feature> &points, std::vector<QgsPointXY> &centers, std::vector<uint> &weights, const int k )
553{
554 const uint n = points.size();
555 std::fill( weights.begin(), weights.end(), 0 );
556 for ( int i = 0; i < k; i++ )
557 {
558 centers[i].setX( 0.0 );
559 centers[i].setY( 0.0 );
560 }
561 for ( uint i = 0; i < n; i++ )
562 {
563 const int cluster = points[i].cluster;
564 centers[cluster] += QgsVector( points[i].point.x(), points[i].point.y() );
565 weights[cluster] += 1;
566 }
567 for ( int i = 0; i < k; i++ )
568 {
569 centers[i] /= weights[i];
570 }
571}
572
573
@ VectorAnyGeometry
Any vector layer with geometry.
Definition qgis.h:3749
@ Point
Point.
Definition qgis.h:296
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
Definition qgis.h:3982
Encapsulates an academic reference and formats it according to style guidelines.
static QgsAcademicReference createPresentation(const QStringList &authors, int year, const QString &title, const QString &meeting, const QString &publisher=QString(), const QString &pages=QString())
Creates a conference paper or presentation reference.
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.
Wraps a request for features to a vector layer (or directly its vector data provider).
@ 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
QgsAttributes attributes
Definition qgsfeature.h:64
QgsFeatureId id
Definition qgsfeature.h:63
void setAttributes(const QgsAttributes &attrs)
Sets the feature's attributes.
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
Encapsulate a field in an attribute table or data source.
Definition qgsfield.h:56
Container of fields for a vector layer.
Definition qgsfields.h:45
bool append(const QgsField &field, Qgis::FieldOrigin origin=Qgis::FieldOrigin::Provider, int originIndex=-1)
Appends a field.
Definition qgsfields.cpp:75
A geometry is the spatial representation of a feature.
const QgsAbstractGeometry * constGet() const
Returns a non-modifiable (const) reference to the underlying abstract geometry primitive.
QgsGeometry centroid() const
Returns the center of mass of a geometry.
Qgis::WkbType wkbType() const
Returns type of the geometry as a WKB type (point / linestring / polygon etc.).
Represents a 2D point.
Definition qgspointxy.h:62
Contains information about the context in which a processing algorithm is executed.
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.
virtual void pushWarning(const QString &warning)
Pushes a warning 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.
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 numeric parameter for processing algorithms.
static QgsFields combineFields(const QgsFields &fieldsA, const QgsFields &fieldsB, const QString &fieldsBPrefix=QString())
Combines two field lists, avoiding duplicate field names (in a case-insensitive manner).
Represent a 2-dimensional vector.
Definition qgsvector.h:34
static Qgis::WkbType flatType(Qgis::WkbType type)
Returns the flat type for a WKB type.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7557
T qgsgeometry_cast(QgsAbstractGeometry *geom)