diff --git a/python/plugins/processing/tests/testdata/custom/circular_strings.gpkg b/python/plugins/processing/tests/testdata/custom/circular_strings.gpkg index dbaa00916ee..128e3c49ba7 100644 Binary files a/python/plugins/processing/tests/testdata/custom/circular_strings.gpkg and b/python/plugins/processing/tests/testdata/custom/circular_strings.gpkg differ diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_lines.gml b/python/plugins/processing/tests/testdata/expected/kmeans_lines.gml new file mode 100644 index 00000000000..339ee24f964 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_lines.gml @@ -0,0 +1,55 @@ + + + + + -1-3 + 115 + + + + + + 6,2 9,2 9,3 11,5 + 1 + + + + + -1,-1 1,-1 + 0 + + + + + 2,0 2,2 3,2 3,3 + 0 + + + + + 3,1 5,1 + 0 + + + + + 7,-3 10,-3 + 1 + + + + + 6,-3 10,1 + 1 + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_lines.xsd b/python/plugins/processing/tests/testdata/expected/kmeans_lines.xsd new file mode 100644 index 00000000000..07d5aac4580 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_lines.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_points_3.gml b/python/plugins/processing/tests/testdata/expected/kmeans_points_3.gml new file mode 100644 index 00000000000..5b4cc8ce748 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_points_3.gml @@ -0,0 +1,86 @@ + + + + + 0-5 + 83 + + + + + + 1,1 + 1 + 2 + 2 + + + + + 3,3 + 2 + 1 + 2 + + + + + 2,2 + 3 + 0 + 2 + + + + + 5,2 + 4 + 2 + 2 + + + + + 4,1 + 5 + 1 + 2 + + + + + 0,-5 + 6 + 0 + 1 + + + + + 8,-1 + 7 + 0 + 0 + + + + + 7,-1 + 8 + 0 + 0 + + + + + 0,-1 + 9 + 0 + 2 + + + diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_points_3.xsd b/python/plugins/processing/tests/testdata/expected/kmeans_points_3.xsd new file mode 100644 index 00000000000..9de54c79467 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_points_3.xsd @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_points_5.gml b/python/plugins/processing/tests/testdata/expected/kmeans_points_5.gml new file mode 100644 index 00000000000..6b1dbf391b6 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_points_5.gml @@ -0,0 +1,86 @@ + + + + + 0-5 + 83 + + + + + + 1,1 + 1 + 2 + 2 + + + + + 3,3 + 2 + 1 + 2 + + + + + 2,2 + 3 + 0 + 2 + + + + + 5,2 + 4 + 2 + 4 + + + + + 4,1 + 5 + 1 + 4 + + + + + 0,-5 + 6 + 0 + 1 + + + + + 8,-1 + 7 + 0 + 0 + + + + + 7,-1 + 8 + 0 + 0 + + + + + 0,-1 + 9 + 0 + 3 + + + diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_points_5.xsd b/python/plugins/processing/tests/testdata/expected/kmeans_points_5.xsd new file mode 100644 index 00000000000..b91f4654aa7 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_points_5.xsd @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_polys.gml b/python/plugins/processing/tests/testdata/expected/kmeans_polys.gml new file mode 100644 index 00000000000..af37bd57d9a --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_polys.gml @@ -0,0 +1,67 @@ + + + + + -1-3 + 106 + + + + + + -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1 + aaaaa + 33 + 44.123456 + 1 + + + + + 5,5 6,4 4,4 5,5 + Aaaaa + -33 + 0 + 1 + + + + + 2,5 2,6 3,6 3,5 2,5 + bbaaa + + 0.123 + 1 + + + + + 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0 + ASDF + 0 + + 0 + + + + + + 120 + -100291.43213 + + + + + + 3,2 6,1 6,-3 2,-1 2,2 3,2 + elim + 2 + 3.33 + 1 + + + diff --git a/python/plugins/processing/tests/testdata/expected/kmeans_polys.xsd b/python/plugins/processing/tests/testdata/expected/kmeans_polys.xsd new file mode 100644 index 00000000000..70f8bdb7fe8 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/kmeans_polys.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index a072d934015..6db7255abd0 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -5660,4 +5660,56 @@ tests: name: expected/vectorize.gml type: vector + - algorithm: native:kmeansclustering + name: K means, points, 3 clusters + params: + CLUSTERS: 3 + FIELD_NAME: CLUSTER_ID + INPUT: + name: points.gml + type: vector + results: + OUTPUT: + name: expected/kmeans_points_3.gml + type: vector + + - algorithm: native:kmeansclustering + name: K means, points, 5 clusters + params: + CLUSTERS: 5 + FIELD_NAME: CLUSTER_ID5 + INPUT: + name: points.gml + type: vector + results: + OUTPUT: + name: expected/kmeans_points_5.gml + type: vector + + - algorithm: native:kmeansclustering + name: K means, lines + params: + CLUSTERS: 2 + FIELD_NAME: CLUSTER_ID + INPUT: + name: lines.gml + type: vector + results: + OUTPUT: + name: expected/kmeans_lines.gml + type: vector + + - algorithm: native:kmeansclustering + name: K means, polys + params: + CLUSTERS: 2 + FIELD_NAME: CLUSTER_ID + INPUT: + name: polys.gml + type: vector + results: + OUTPUT: + name: expected/kmeans_polys.gml + type: vector + # See ../README.md for a description of the file format diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 26b83289af2..b032e0b8ab5 100755 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -41,10 +41,11 @@ SET(QGIS_ANALYSIS_SRCS processing/qgsalgorithmfiledownloader.cpp processing/qgsalgorithmfilter.cpp processing/qgsalgorithmfixgeometries.cpp + processing/qgsalgorithmimportphotos.cpp processing/qgsalgorithmintersection.cpp processing/qgsalgorithmjoinbyattribute.cpp processing/qgsalgorithmjoinwithlines.cpp - processing/qgsalgorithmimportphotos.cpp + processing/qgsalgorithmkmeansclustering.cpp processing/qgsalgorithmlineintersection.cpp processing/qgsalgorithmloadlayer.cpp processing/qgsalgorithmmeancoordinates.cpp diff --git a/src/analysis/processing/qgsalgorithmkmeansclustering.cpp b/src/analysis/processing/qgsalgorithmkmeansclustering.cpp new file mode 100644 index 00000000000..717dde4ef17 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmkmeansclustering.cpp @@ -0,0 +1,371 @@ +/*************************************************************************** + qgsalgorithmkmeansclustering.cpp + --------------------- + begin : June 2018 + copyright : (C) 2018 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsalgorithmkmeansclustering.h" + +///@cond PRIVATE + +const int KMEANS_MAX_ITERATIONS = 1000; + +QString QgsKMeansClusteringAlgorithm::name() const +{ + return QStringLiteral( "kmeansclustering" ); +} + +QString QgsKMeansClusteringAlgorithm::displayName() const +{ + return QObject::tr( "K-means clustering" ); +} + +QStringList QgsKMeansClusteringAlgorithm::tags() const +{ + return QObject::tr( "clustering,clusters,kmeans,points" ).split( ',' ); +} + +QString QgsKMeansClusteringAlgorithm::group() const +{ + return QObject::tr( "Vector analysis" ); +} + +QString QgsKMeansClusteringAlgorithm::groupId() const +{ + return QStringLiteral( "vectoranalysis" ); +} + +void QgsKMeansClusteringAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), + QObject::tr( "Input layer" ), QList< int >() << QgsProcessing::TypeVectorAnyGeometry ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "CLUSTERS" ), QObject::tr( "Number of clusters" ), + QgsProcessingParameterNumber::Integer, 5, false, 1 ) ); + addParameter( new QgsProcessingParameterString( QStringLiteral( "FIELD_NAME" ), + QObject::tr( "Cluster field name" ), QStringLiteral( "CLUSTER_ID" ) ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Clusters" ), QgsProcessing::TypeVectorAnyGeometry ) ); +} + +QString QgsKMeansClusteringAlgorithm::shortHelpString() const +{ + return QObject::tr( "Calculates the 2D distance based k-means cluster number for each input feature.\n\n" + "If input geometries are line or polygons, the clustering is based on the centroid of the feature." ); +} + +QgsKMeansClusteringAlgorithm *QgsKMeansClusteringAlgorithm::createInstance() const +{ + return new QgsKMeansClusteringAlgorithm(); +} + +QVariantMap QgsKMeansClusteringAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + std::unique_ptr< QgsProcessingFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !source ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + int k = parameterAsInt( parameters, QStringLiteral( "CLUSTERS" ), context ); + + QgsFields outputFields = source->fields(); + const QString clusterFieldName = parameterAsString( parameters, QStringLiteral( "FIELD_NAME" ), context ); + QgsFields newFields; + newFields.append( QgsField( clusterFieldName, QVariant::Int ) ); + outputFields = QgsProcessingUtils::combineFields( outputFields, newFields ); + + QString dest; + std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, outputFields, source->wkbType(), source->sourceCrs() ) ); + if ( !sink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "OUTPUT" ) ) ); + + // build list of point inputs - if it's already a point, use that. If not, take the centroid. + feedback->pushInfo( QObject::tr( "Collecting input points" ) ); + double step = source->featureCount() > 0 ? 50.0 / source->featureCount() : 1; + int i = 0; + int n = 0; + QgsFeature feat; + + std::vector< Feature > clusterFeatures; + QgsFeatureIterator features = source->getFeatures( QgsFeatureRequest().setSubsetOfAttributes( QgsAttributeList() ) ); + QHash< QgsFeatureId, int > idToObj; + while ( features.nextFeature( feat ) ) + { + i++; + if ( feedback->isCanceled() ) + { + break; + } + + feedback->setProgress( i * step ); + if ( !feat.hasGeometry() ) + continue; + + n++; + + QgsPointXY point; + if ( QgsWkbTypes::flatType( feat.geometry().wkbType() ) == QgsWkbTypes::Point ) + point = QgsPointXY( *qgsgeometry_cast< const QgsPoint * >( feat.geometry().constGet() ) ); + else + { + QgsGeometry centroid = feat.geometry().centroid(); + point = QgsPointXY( *qgsgeometry_cast< const QgsPoint * >( centroid.constGet() ) ); + } + + idToObj.insert( feat.id(), clusterFeatures.size() ); + clusterFeatures.emplace_back( Feature( point ) ); + } + + if ( n < k ) + { + feedback->reportError( QObject::tr( "Number of geometries is less than the number of clusters requested, not all clusters will get data" ) ); + k = n; + } + + if ( k > 1 ) + { + feedback->pushInfo( QObject::tr( "Calculating clusters" ) ); + + // cluster centers + std::vector< QgsPointXY > centers( k ); + + initClusters( clusterFeatures, centers, k, feedback ); + calculateKMeans( clusterFeatures, centers, k, feedback ); + } + + features = source->getFeatures(); + i = 0; + while ( features.nextFeature( feat ) ) + { + i++; + if ( feedback->isCanceled() ) + { + break; + } + + feedback->setProgress( 50 + i * step ); + QgsAttributes attr = feat.attributes(); + if ( !feat.hasGeometry() ) + { + attr << QVariant(); + } + else if ( k <= 1 ) + { + attr << 0; + } + else if ( !idToObj.contains( feat.id() ) ) + { + attr << QVariant(); + } + else + { + attr << clusterFeatures[ idToObj.value( feat.id() ) ].cluster; + } + feat.setAttributes( attr ); + sink->addFeature( feat, QgsFeatureSink::FastInsert ); + } + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), dest ); + return outputs; +} + +// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c + +void QgsKMeansClusteringAlgorithm::initClusters( std::vector &points, std::vector ¢ers, const int k, QgsProcessingFeedback *feedback ) +{ + ulong n = points.size(); + if ( n == 0 ) + return; + + if ( n == 1 ) + { + for ( int i = 0; i < k; i++ ) + centers[ i ] = points[ 0 ].point; + return; + } + + long duplicateCount = 1; + // initially scan for two most distance points from each other, p1 and p2 + ulong p1 = 0; + ulong p2 = 0; + double distanceP1 = 0; + double distanceP2 = 0; + double maxDistance = -1; + for ( ulong i = 1; i < n; i++ ) + { + distanceP1 = points[i].point.sqrDist( points[p1].point ); + distanceP2 = points[i].point.sqrDist( points[p2].point ); + + // if this point is further then existing candidates, replace our choice + if ( ( distanceP1 > maxDistance ) || ( distanceP2 > maxDistance ) ) + { + maxDistance = std::max( distanceP1, distanceP2 ); + if ( distanceP1 > distanceP2 ) + p2 = i; + else + p1 = i; + } + + // also record count of duplicate points + if ( qgsDoubleNear( distanceP1, 0 ) || qgsDoubleNear( distanceP2, 0 ) ) + duplicateCount++; + } + + if ( feedback && duplicateCount > 1 ) + { + feedback->pushInfo( QObject::tr( "There are at least %1 duplicate inputs, the number of output clusters may be less than was requested" ).arg( duplicateCount ) ); + } + + // By now two points should be found and be not the same + Q_ASSERT( p1 != p2 && maxDistance >= 0 ); + + // Accept these two points as our initial centers + centers[0] = points[p1].point; + centers[1] = points[p2].point; + + if ( k > 2 ) + { + // array of minimum distance to a point from accepted cluster centers + std::vector< double > distances( n ); + + // initialize array with distance to first object + for ( ulong j = 0; j < n; j++ ) + { + distances[j] = points[j].point.sqrDist( centers[0] ); + } + distances[p1] = -1; + distances[p2] = -1; + + // loop i on clusters, skip 0 and 1 as found already + for ( int i = 2; i < k; i++ ) + { + ulong candidateCenter = 0; + double maxDistance = std::numeric_limits::lowest(); + + // loop j on points + for ( ulong j = 0; j < n; j++ ) + { + // accepted clusters are already marked with distance = -1 + if ( distances[j] < 0 ) + continue; + + // update minimal distance with previously accepted cluster + distances[j] = std::min( points[j].point.sqrDist( centers[i - 1] ), distances[j] ); + + // greedily take a point that's farthest from any of accepted clusters + if ( distances[j] > maxDistance ) + { + candidateCenter = j; + maxDistance = distances[j]; + } + } + + // checked earlier by counting entries on input, just in case + Q_ASSERT( maxDistance >= 0 ); + + // accept candidate to centers + distances[candidateCenter] = -1; + // copy the point coordinates into the initial centers array + centers[i] = points[candidateCenter].point; + } + } +} + +// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c + +void QgsKMeansClusteringAlgorithm::calculateKMeans( std::vector &objs, std::vector ¢ers, int k, QgsProcessingFeedback *feedback ) +{ + int converged = false; + bool changed = false; + + // avoid reallocating weights array for every iteration + std::vector< uint > weights( k ); + + uint i = 0; + for ( i = 0; i < KMEANS_MAX_ITERATIONS && !converged; i++ ) + { + if ( feedback && feedback->isCanceled() ) + break; + + findNearest( objs, centers, k, changed ); + updateMeans( objs, centers, weights, k ); + converged = !changed; + } + + if ( !converged && feedback ) + feedback->reportError( QObject::tr( "Clustering did not converge after %1 iterations" ).arg( i ) ); + else if ( feedback ) + feedback->pushInfo( QObject::tr( "Clustering converged after %1 iterations" ).arg( i ) ); +} + +// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c + +void QgsKMeansClusteringAlgorithm::findNearest( std::vector &points, const std::vector ¢ers, const int k, bool &changed ) +{ + changed = false; + ulong n = points.size(); + for ( ulong i = 0; i < n; i++ ) + { + Feature &point = points[i]; + + // Initialize with distance to first cluster + double currentDistance = point.point.sqrDist( centers[0] ); + int currentCluster = 0; + + // Check all other cluster centers and find the nearest + for ( int cluster = 1; cluster < k; cluster++ ) + { + const double distance = point.point.sqrDist( centers[cluster] ); + if ( distance < currentDistance ) + { + currentDistance = distance; + currentCluster = cluster; + } + } + + // Store the nearest cluster this object is in + if ( point.cluster != currentCluster ) + { + changed = true; + point.cluster = currentCluster; + } + } +} + +// ported from https://github.com/postgis/postgis/blob/svn-trunk/liblwgeom/lwkmeans.c + +void QgsKMeansClusteringAlgorithm::updateMeans( const std::vector &points, std::vector ¢ers, std::vector &weights, const int k ) +{ + uint n = points.size(); + std::fill( weights.begin(), weights.end(), 0 ); + for ( int i = 0; i < k; i++ ) + { + centers[i].setX( 0.0 ); + centers[i].setY( 0.0 ); + } + for ( uint i = 0; i < n; i++ ) + { + int cluster = points[i].cluster; + centers[cluster] += QgsVector( points[i].point.x(), + points[i].point.y() ); + weights[cluster] += 1; + } + for ( int i = 0; i < k; i++ ) + { + centers[i] /= weights[i]; + } +} + + +///@endcond + + diff --git a/src/analysis/processing/qgsalgorithmkmeansclustering.h b/src/analysis/processing/qgsalgorithmkmeansclustering.h new file mode 100644 index 00000000000..3ec8a2c3dd3 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmkmeansclustering.h @@ -0,0 +1,77 @@ +/*************************************************************************** + qgsalgorithmkmeansclustering.h + --------------------- + begin : June 2018 + copyright : (C) 2018 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSALGORITHMKMEANSCLUSTERING_H +#define QGSALGORITHMKMEANSCLUSTERING_H + +#define SIP_NO_FILE + +#include "qgis.h" +#include "qgis_analysis.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + + +/** + * Native k-means clustering algorithm. + */ +class ANALYSIS_EXPORT QgsKMeansClusteringAlgorithm : public QgsProcessingAlgorithm +{ + + public: + + QgsKMeansClusteringAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QgsKMeansClusteringAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + private: + + struct Feature + { + Feature( QgsPointXY point ) + : point( point ) + {} + + QgsPointXY point; + int cluster = -1; + }; + + static void initClusters( std::vector< Feature > &points, std::vector< QgsPointXY > ¢ers, int k, QgsProcessingFeedback *feedback ); + static void calculateKMeans( std::vector< Feature > &points, std::vector< QgsPointXY > ¢ers, int k, QgsProcessingFeedback *feedback ); + static void findNearest( std::vector< Feature > &points, const std::vector< QgsPointXY > ¢ers, int k, bool &changed ); + static void updateMeans( const std::vector< Feature > &points, std::vector< QgsPointXY > ¢ers, std::vector< uint > &weights, int k ); + + friend class TestQgsProcessingAlgs; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMKMEANSCLUSTERING_H + + diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index eea2ebfe0c8..1285bdd7aaf 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -42,6 +42,7 @@ #include "qgsalgorithmjoinwithlines.h" #include "qgsalgorithmimportphotos.h" #include "qgsalgorithmintersection.h" +#include "qgsalgorithmkmeansclustering.h" #include "qgsalgorithmlineintersection.h" #include "qgsalgorithmloadlayer.h" #include "qgsalgorithmmeancoordinates.h" @@ -150,6 +151,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsIntersectionAlgorithm() ); addAlgorithm( new QgsJoinByAttributeAlgorithm() ); addAlgorithm( new QgsJoinWithLinesAlgorithm() ); + addAlgorithm( new QgsKMeansClusteringAlgorithm() ); addAlgorithm( new QgsLineIntersectionAlgorithm() ); addAlgorithm( new QgsLoadLayerAlgorithm() ); addAlgorithm( new QgsMeanCoordinatesAlgorithm() ); diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index c3459a6b12c..06feb05e233 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -1870,6 +1870,12 @@ QgsGeometry QgsGeometry::centroid() const return QgsGeometry(); } + // avoid calling geos for trivial point centroids + if ( QgsWkbTypes::flatType( d->geometry->wkbType() ) == QgsWkbTypes::Point ) + { + return *this; + } + QgsGeos geos( d->geometry.get() ); mLastError.clear(); diff --git a/tests/src/analysis/testqgsprocessingalgs.cpp b/tests/src/analysis/testqgsprocessingalgs.cpp index 1b9a21192f1..1934a2c18e5 100644 --- a/tests/src/analysis/testqgsprocessingalgs.cpp +++ b/tests/src/analysis/testqgsprocessingalgs.cpp @@ -25,6 +25,7 @@ #include "qgsnativealgorithms.h" #include "qgsalgorithmimportphotos.h" #include "qgsalgorithmtransform.h" +#include "qgsalgorithmkmeansclustering.h" class TestQgsProcessingAlgs: public QObject { @@ -41,6 +42,7 @@ class TestQgsProcessingAlgs: public QObject void parseGeoTags(); void featureFilterAlg(); void transformAlg(); + void kmeansCluster(); private: @@ -435,6 +437,64 @@ void TestQgsProcessingAlgs::transformAlg() QVERIFY( ok ); } +void TestQgsProcessingAlgs::kmeansCluster() +{ + // make some features + std::vector< QgsKMeansClusteringAlgorithm::Feature > features; + std::vector< QgsPointXY > centers( 2 ); + + // no features, no crash + int k = 2; + QgsKMeansClusteringAlgorithm::initClusters( features, centers, k, nullptr ); + QgsKMeansClusteringAlgorithm::calculateKMeans( features, centers, k, nullptr ); + + // features < clusters + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 1, 5 ) ) ); + QgsKMeansClusteringAlgorithm::initClusters( features, centers, k, nullptr ); + QgsKMeansClusteringAlgorithm::calculateKMeans( features, centers, k, nullptr ); + QCOMPARE( features[ 0 ].cluster, 0 ); + + // features == clusters + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 11, 5 ) ) ); + QgsKMeansClusteringAlgorithm::initClusters( features, centers, k, nullptr ); + QgsKMeansClusteringAlgorithm::calculateKMeans( features, centers, k, nullptr ); + QCOMPARE( features[ 0 ].cluster, 1 ); + QCOMPARE( features[ 1 ].cluster, 0 ); + + // features > clusters + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 13, 3 ) ) ); + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 13, 13 ) ) ); + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 23, 6 ) ) ); + k = 2; + QgsKMeansClusteringAlgorithm::initClusters( features, centers, k, nullptr ); + QgsKMeansClusteringAlgorithm::calculateKMeans( features, centers, k, nullptr ); + QCOMPARE( features[ 0 ].cluster, 1 ); + QCOMPARE( features[ 1 ].cluster, 1 ); + QCOMPARE( features[ 2 ].cluster, 0 ); + QCOMPARE( features[ 3 ].cluster, 0 ); + QCOMPARE( features[ 4 ].cluster, 0 ); + + // repeat above, with 3 clusters + k = 3; + centers.resize( 3 ); + QgsKMeansClusteringAlgorithm::initClusters( features, centers, k, nullptr ); + QgsKMeansClusteringAlgorithm::calculateKMeans( features, centers, k, nullptr ); + QCOMPARE( features[ 0 ].cluster, 1 ); + QCOMPARE( features[ 1 ].cluster, 2 ); + QCOMPARE( features[ 2 ].cluster, 2 ); + QCOMPARE( features[ 3 ].cluster, 2 ); + QCOMPARE( features[ 4 ].cluster, 0 ); + + // with identical points + features.clear(); + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 1, 5 ) ) ); + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 1, 5 ) ) ); + features.emplace_back( QgsKMeansClusteringAlgorithm::Feature( QgsPointXY( 1, 5 ) ) ); + QCOMPARE( features[ 0 ].cluster, -1 ); + QCOMPARE( features[ 1 ].cluster, -1 ); + QCOMPARE( features[ 2 ].cluster, -1 ); +} + QGSTEST_MAIN( TestQgsProcessingAlgs ) #include "testqgsprocessingalgs.moc"