QGIS API Documentation 4.3.0-Master (ffcfc20b9b4)
Loading...
Searching...
No Matches
qgsalgorithmdbscanclustering.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmdbscanclustering.cpp
3 ---------------------
4 begin : July 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 <unordered_set>
21
23
24#include <QString>
25
26using namespace Qt::StringLiterals;
27
29
30QString QgsDbscanClusteringAlgorithm::name() const
31{
32 return u"dbscanclustering"_s;
33}
34
35QString QgsDbscanClusteringAlgorithm::displayName() const
36{
37 return QObject::tr( "DBSCAN clustering" );
38}
39
40QString QgsDbscanClusteringAlgorithm::shortDescription() const
41{
42 return QObject::tr( "Clusters point features using a density based scan algorithm." );
43}
44
45QStringList QgsDbscanClusteringAlgorithm::tags() const
46{
47 return QObject::tr( "clustering,clusters,density,based,points,distance" ).split( ',' );
48}
49
50QString QgsDbscanClusteringAlgorithm::group() const
51{
52 return QObject::tr( "Vector analysis" );
53}
54
55QString QgsDbscanClusteringAlgorithm::groupId() const
56{
57 return u"vectoranalysis"_s;
58}
59
60void QgsDbscanClusteringAlgorithm::initAlgorithm( const QVariantMap & )
61{
62 addParameter( new QgsProcessingParameterFeatureSource( u"INPUT"_s, QObject::tr( "Input layer" ), QList<int>() << static_cast<int>( Qgis::ProcessingSourceType::VectorPoint ) ) );
63 addParameter( new QgsProcessingParameterNumber( u"MIN_SIZE"_s, QObject::tr( "Minimum cluster size" ), Qgis::ProcessingNumberParameterType::Integer, 5, false, 1 ) );
64 addParameter( new QgsProcessingParameterDistance( u"EPS"_s, QObject::tr( "Maximum distance between clustered points" ), 1, u"INPUT"_s, false, 0 ) );
65
66 auto dbscanStarParam = std::make_unique<QgsProcessingParameterBoolean>( u"DBSCAN*"_s, QObject::tr( "Treat border points as noise (DBSCAN*)" ), false );
67 dbscanStarParam->setFlags( dbscanStarParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
68 addParameter( dbscanStarParam.release() );
69
70 auto fieldNameParam = std::make_unique<QgsProcessingParameterString>( u"FIELD_NAME"_s, QObject::tr( "Cluster field name" ), u"CLUSTER_ID"_s );
71 fieldNameParam->setFlags( fieldNameParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
72 addParameter( fieldNameParam.release() );
73 auto sizeFieldNameParam = std::make_unique<QgsProcessingParameterString>( u"SIZE_FIELD_NAME"_s, QObject::tr( "Cluster size field name" ), u"CLUSTER_SIZE"_s );
74 sizeFieldNameParam->setFlags( sizeFieldNameParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
75 addParameter( sizeFieldNameParam.release() );
76
77 addParameter( new QgsProcessingParameterFeatureSink( u"OUTPUT"_s, QObject::tr( "Clusters" ), Qgis::ProcessingSourceType::VectorPoint ) );
78
79 addOutput( new QgsProcessingOutputNumber( u"NUM_CLUSTERS"_s, QObject::tr( "Number of clusters" ) ) );
80}
81
82QString QgsDbscanClusteringAlgorithm::shortHelpString() const
83{
84 return QObject::tr(
85 "This algorithm clusters point features based on a 2D implementation of Density-based spatial clustering of applications with noise (DBSCAN) algorithm.\n\n"
86 "The algorithm requires two parameters, a minimum cluster size (“minPts”), and the maximum distance allowed between clustered points (“eps”)."
87 );
88}
89
90QgsDbscanClusteringAlgorithm *QgsDbscanClusteringAlgorithm::createInstance() const
91{
92 return new QgsDbscanClusteringAlgorithm();
93}
94
95struct KDBushDataEqualById
96{
97 bool operator()( const QgsSpatialIndexKDBushData &a, const QgsSpatialIndexKDBushData &b ) const { return a.id == b.id; }
98};
99
100struct KDBushDataHashById
101{
102 std::size_t operator()( const QgsSpatialIndexKDBushData &a ) const { return std::hash<QgsFeatureId> {}( a.id ); }
103};
104
105QVariantMap QgsDbscanClusteringAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
106{
107 QGS_MARK_ALGORITHM_SOURCE
108
109 std::unique_ptr<QgsProcessingFeatureSource> source( parameterAsSource( parameters, u"INPUT"_s, context ) );
110 if ( !source )
111 throw QgsProcessingException( invalidSourceError( parameters, u"INPUT"_s ) );
112
113 const std::size_t minSize = static_cast<std::size_t>( parameterAsInt( parameters, u"MIN_SIZE"_s, context ) );
114 const double eps1 = parameterAsDouble( parameters, u"EPS"_s, context );
115 const double eps2 = parameterAsDouble( parameters, u"EPS2"_s, context );
116 const bool borderPointsAreNoise = parameterAsBoolean( parameters, u"DBSCAN*"_s, context );
117
118 QgsFields outputFields = source->fields();
119 QgsFields newFields;
120 const QString clusterFieldName = parameterAsString( parameters, u"FIELD_NAME"_s, context );
121 newFields.append( QgsField( clusterFieldName, QMetaType::Type::Int ) );
122 const QString clusterSizeFieldName = parameterAsString( parameters, u"SIZE_FIELD_NAME"_s, context );
123 newFields.append( QgsField( clusterSizeFieldName, QMetaType::Type::Int ) );
124 outputFields = QgsProcessingUtils::combineFields( outputFields, newFields );
125
126 QString dest;
127 std::unique_ptr<QgsFeatureSink> sink( parameterAsSink( parameters, u"OUTPUT"_s, context, dest, outputFields, source->wkbType(), source->sourceCrs() ) );
128 if ( !sink )
129 throw QgsProcessingException( invalidSinkError( parameters, u"OUTPUT"_s ) );
130
131 QgsFeatureRequest indexRequest;
132
133 std::unordered_map<QgsFeatureId, QDateTime> idToDateTime;
134 const QString dateTimeFieldName = parameterAsString( parameters, u"DATETIME_FIELD"_s, context );
135 int dateTimefieldIndex = -1;
136 if ( !dateTimeFieldName.isEmpty() )
137 {
138 dateTimefieldIndex = source->fields().lookupField( dateTimeFieldName );
139 if ( dateTimefieldIndex == -1 )
140 throw QgsProcessingException( QObject::tr( "Datetime field missing" ) );
141
142 indexRequest.setSubsetOfAttributes( QgsAttributeList() << dateTimefieldIndex );
143 }
144 else
145 {
146 indexRequest.setNoAttributes();
147 }
148
149 // build spatial index, also collecting feature datetimes if required
150 feedback->pushInfo( QObject::tr( "Building spatial index" ) );
151 QgsFeatureIterator indexIterator = source->getFeatures( indexRequest );
153 indexIterator,
154 [&idToDateTime, dateTimefieldIndex]( const QgsFeature &feature ) -> bool {
155 if ( dateTimefieldIndex >= 0 )
156 idToDateTime[feature.id()] = feature.attributes().at( dateTimefieldIndex ).toDateTime();
157 return true;
158 },
159 feedback
160 );
161
162 if ( feedback->isCanceled() )
163 return QVariantMap();
164
165 // stdbscan!
166 feedback->pushInfo( QObject::tr( "Analysing clusters" ) );
167 std::unordered_map<QgsFeatureId, int> idToCluster;
168 idToCluster.reserve( index.size() );
169 const long featureCount = source->featureCount();
170 QgsFeatureIterator features = source->getFeatures( QgsFeatureRequest().setNoAttributes() );
171 stdbscan( minSize, eps1, eps2, borderPointsAreNoise, featureCount, features, index, idToCluster, idToDateTime, feedback );
172
173 // cluster size
174 std::unordered_map<int, int> clusterSize;
175 std::for_each( idToCluster.begin(), idToCluster.end(), [&clusterSize]( std::pair<QgsFeatureId, int> idCluster ) { clusterSize[idCluster.second]++; } );
176
177 // write clusters
178 const double writeStep = featureCount > 0 ? 10.0 / featureCount : 1;
179 features = source->getFeatures();
180 int i = 0;
181 QgsFeature feat;
182 while ( features.nextFeature( feat ) )
183 {
184 i++;
185 if ( feedback->isCanceled() )
186 {
187 break;
188 }
189
190 feedback->setProgress( 90 + i * writeStep );
191 QgsAttributes attr = feat.attributes();
192 const auto cluster = idToCluster.find( feat.id() );
193 if ( cluster != idToCluster.end() )
194 {
195 attr << cluster->second << clusterSize[cluster->second];
196 }
197 else
198 {
199 attr << QVariant() << QVariant();
200 }
201 feat.setAttributes( attr );
202 if ( !sink->addFeature( feat, QgsFeatureSink::FastInsert ) )
203 throw QgsProcessingException( writeFeatureError( sink.get(), parameters, u"OUTPUT"_s ) );
204 else
205 feedback->featureAddedToSink( u"OUTPUT"_s );
206 }
207
208 sink->finalize();
209 feedback->featureSinkFinalized( u"OUTPUT"_s );
210
211 QVariantMap outputs;
212 outputs.insert( u"OUTPUT"_s, dest );
213 outputs.insert( u"NUM_CLUSTERS"_s, static_cast<unsigned int>( clusterSize.size() ) );
214 return outputs;
215}
216
217void QgsDbscanClusteringAlgorithm::stdbscan(
218 const std::size_t minSize,
219 const double eps1,
220 const double eps2,
221 const bool borderPointsAreNoise,
222 const long featureCount,
223 QgsFeatureIterator features,
225 std::unordered_map<QgsFeatureId, int> &idToCluster,
226 std::unordered_map<QgsFeatureId, QDateTime> &idToDateTime,
227 QgsProcessingFeedback *feedback
228)
229{
230 const double step = featureCount > 0 ? 90.0 / featureCount : 1;
231
232 std::unordered_set<QgsFeatureId> visited;
233 visited.reserve( index.size() );
234
235 QgsFeature feat;
236 int i = 0;
237 int clusterCount = 0;
238
239 while ( features.nextFeature( feat ) )
240 {
241 if ( feedback->isCanceled() )
242 {
243 break;
244 }
245
246 if ( !feat.hasGeometry() )
247 {
248 feedback->setProgress( ++i * step );
249 continue;
250 }
251
252 if ( visited.find( feat.id() ) != visited.end() )
253 {
254 // already visited!
255 continue;
256 }
257
258 QgsPointXY point;
261 else
262 {
263 // not a point geometry
264 feedback->reportError( QObject::tr( "Feature %1 is a %2 feature, not a point." ).arg( feat.id() ).arg( QgsWkbTypes::displayString( feat.geometry().wkbType() ) ) );
265 feedback->setProgress( ++i * step );
266 continue;
267 }
268
269 if ( !idToDateTime.empty() && !idToDateTime[feat.id()].isValid() )
270 {
271 // missing datetime value
272 feedback->reportError( QObject::tr( "Feature %1 is missing a valid datetime value." ).arg( feat.id() ).arg( QgsWkbTypes::displayString( feat.geometry().wkbType() ) ) );
273 feedback->setProgress( ++i * step );
274 continue;
275 }
276
277 std::unordered_set<QgsSpatialIndexKDBushData, KDBushDataHashById, KDBushDataEqualById> within;
278
279 if ( minSize > 1 )
280 {
281 index.within( point, eps1, [&within, pointId = feat.id(), &idToDateTime, &eps2]( const QgsSpatialIndexKDBushData &data ) {
282 if ( idToDateTime.empty() || ( idToDateTime[data.id].isValid() && std::abs( idToDateTime[pointId].msecsTo( idToDateTime[data.id] ) ) <= eps2 ) )
283 within.insert( data );
284 } );
285 if ( within.size() < minSize )
286 continue;
287
288 visited.insert( feat.id() );
289 }
290 else
291 {
292 // optimised case for minSize == 1, we can skip the initial check
293 within.insert( QgsSpatialIndexKDBushData( feat.id(), point.x(), point.y() ) );
294 }
295
296 // start new cluster
297 clusterCount++;
298 idToCluster[feat.id()] = clusterCount;
299 feedback->setProgress( ++i * step );
300
301 while ( !within.empty() )
302 {
303 if ( feedback->isCanceled() )
304 {
305 break;
306 }
307
308 const QgsSpatialIndexKDBushData j = *within.begin();
309 within.erase( within.begin() );
310
311 if ( visited.find( j.id ) != visited.end() )
312 {
313 // already visited!
314 continue;
315 }
316
317 visited.insert( j.id );
318 feedback->setProgress( ++i * step );
319
320 // check from this point
321 const QgsPointXY point2 = j.point();
322
323 std::unordered_set<QgsSpatialIndexKDBushData, KDBushDataHashById, KDBushDataEqualById> within2;
324 index.within( point2, eps1, [&within2, point2Id = j.id, &idToDateTime, &eps2]( const QgsSpatialIndexKDBushData &data ) {
325 if ( idToDateTime.empty() || ( idToDateTime[data.id].isValid() && std::abs( idToDateTime[point2Id].msecsTo( idToDateTime[data.id] ) ) <= eps2 ) )
326 within2.insert( data );
327 } );
328
329 if ( within2.size() >= minSize )
330 {
331 // expand neighbourhood
332 std::copy_if( within2.begin(), within2.end(), std::inserter( within, within.end() ), [&visited]( const QgsSpatialIndexKDBushData &needle ) {
333 return visited.find( needle.id ) == visited.end();
334 } );
335 }
336 if ( !borderPointsAreNoise || within2.size() >= minSize )
337 {
338 idToCluster[j.id] = clusterCount;
339 }
340 }
341 }
342}
343
@ VectorPoint
Vector point layers.
Definition qgis.h:3750
@ Point
Point.
Definition qgis.h:296
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
Definition qgis.h:3982
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).
QgsFeatureRequest & setSubsetOfAttributes(const QgsAttributeList &attrs)
Set a subset of attributes that will be fetched.
QgsFeatureRequest & setNoAttributes()
Set that no attributes will be fetched.
@ 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
const QgsAbstractGeometry * constGet() const
Returns a non-modifiable (const) reference to the underlying abstract geometry primitive.
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
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
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.
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.
A double numeric parameter for distance 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).
A container for data stored inside a QgsSpatialIndexKDBush index.
QgsPointXY point() const
Returns the indexed point.
A very fast static spatial index for 2D points based on a flat KD-tree.
qgssize size() const
Returns the size of the index, i.e.
QList< QgsSpatialIndexKDBushData > within(const QgsPointXY &point, double radius) const
Returns the list of features which are within the given search radius of point.
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 Qgis::WkbType flatType(Qgis::WkbType type)
Returns the flat type for a WKB type.
T qgsgeometry_cast(QgsAbstractGeometry *geom)
QList< int > QgsAttributeList
Definition qgsfield.h:30