From 0dd434c1837c3ac3d1bed66b44492ad2a21d9f1a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Sep 2017 09:25:18 +1000 Subject: [PATCH] [processing] Port mean coordinates to c++ And remove final use of inefficient vector.extractPoints function --- python/plugins/processing/algs/help/qgis.yaml | 8 - .../processing/algs/qgis/MeanCoords.py | 177 ------------------ .../algs/qgis/QGISAlgorithmProvider.py | 2 - .../tests/testdata/qgis_algorithm_tests.yaml | 49 ++++- python/plugins/processing/tools/vector.py | 28 --- src/core/processing/qgsnativealgorithms.cpp | 174 +++++++++++++++++ src/core/processing/qgsnativealgorithms.h | 24 +++ 7 files changed, 246 insertions(+), 216 deletions(-) delete mode 100644 python/plugins/processing/algs/qgis/MeanCoords.py diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index a9443a1af15..226d62dcfd0 100755 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -294,14 +294,6 @@ qgis:listuniquevalues: > qgis:meanandstandarddeviationplot: - -qgis:meancoordinates: > - This algorithm computes a point layer with the center of mass of geometries in an input layer. - - An attribute can be specified as containing weights to be applied to each feature when computing the center of mass. - - If an attribute is selected in the parameters, features will be grouped according to values in this field. Instead of a single point with the center of mass of the whole layer, the output layer will contain a center of mass for the features in each category. - qgis:mergevectorlayers: > This algorithm combines multiple vector layers of the same geometry type into a single one. diff --git a/python/plugins/processing/algs/qgis/MeanCoords.py b/python/plugins/processing/algs/qgis/MeanCoords.py deleted file mode 100644 index eb09504640a..00000000000 --- a/python/plugins/processing/algs/qgis/MeanCoords.py +++ /dev/null @@ -1,177 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - MeanCoords.py - --------------------- - Date : August 2012 - Copyright : (C) 2012 by Victor Olaya - Email : volayaf 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. * -* * -*************************************************************************** -""" -from builtins import str - -__author__ = 'Victor Olaya' -__date__ = 'August 2012' -__copyright__ = '(C) 2012, Victor Olaya' - -# This will get replaced with a git SHA1 when you do a git archive - -__revision__ = '$Format:%H$' - -import os - -from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtCore import QVariant - -from qgis.core import (QgsField, - QgsFeature, - QgsGeometry, - QgsPointXY, - QgsWkbTypes, - QgsFeatureRequest, - QgsFeatureSink, - QgsFields, - QgsProcessing, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - QgsProcessingException) - -from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.tools import vector - -pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] - - -class MeanCoords(QgisAlgorithm): - INPUT = 'INPUT' - WEIGHT = 'WEIGHT' - OUTPUT = 'OUTPUT' - UID = 'UID' - WEIGHT = 'WEIGHT' - - def icon(self): - return QIcon(os.path.join(pluginPath, 'images', 'ftools', 'mean.png')) - - def group(self): - return self.tr('Vector analysis') - - def __init__(self): - super().__init__() - - def initAlgorithm(self, config=None): - self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, - self.tr('Input layer'))) - self.addParameter(QgsProcessingParameterField(self.WEIGHT, self.tr('Weight field'), - parentLayerParameterName=MeanCoords.INPUT, - type=QgsProcessingParameterField.Numeric, - optional=True)) - self.addParameter(QgsProcessingParameterField(self.UID, - self.tr('Unique ID field'), - parentLayerParameterName=MeanCoords.INPUT, - optional=True)) - - self.addParameter(QgsProcessingParameterFeatureSink(MeanCoords.OUTPUT, self.tr('Mean coordinates'), - QgsProcessing.TypeVectorPoint)) - - def name(self): - return 'meancoordinates' - - def displayName(self): - return self.tr('Mean coordinate(s)') - - def processAlgorithm(self, parameters, context, feedback): - source = self.parameterAsSource(parameters, self.INPUT, context) - - weight_field = self.parameterAsString(parameters, self.WEIGHT, context) - unique_field = self.parameterAsString(parameters, self.UID, context) - - attributes = [] - if not weight_field: - weight_index = -1 - else: - weight_index = source.fields().lookupField(weight_field) - if weight_index >= 0: - attributes.append(weight_index) - - if not unique_field: - unique_index = -1 - else: - unique_index = source.fields().lookupField(unique_field) - if unique_index >= 0: - attributes.append(unique_index) - - field_list = QgsFields() - field_list.append(QgsField('MEAN_X', QVariant.Double, '', 24, 15)) - field_list.append(QgsField('MEAN_Y', QVariant.Double, '', 24, 15)) - if unique_index >= 0: - field_list.append(QgsField('UID', QVariant.String, '', 255)) - - (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, - field_list, QgsWkbTypes.Point, source.sourceCrs()) - - features = source.getFeatures(QgsFeatureRequest().setSubsetOfAttributes(attributes)) - total = 100.0 / source.featureCount() if source.featureCount() else 0 - means = {} - for current, feat in enumerate(features): - if feedback.isCanceled(): - break - - feedback.setProgress(int(current * total)) - if unique_index == -1: - clazz = "Single class" - else: - clazz = str(feat.attributes()[unique_index]).strip() - if weight_index == -1: - weight = 1.00 - else: - try: - weight = float(feat.attributes()[weight_index]) - except: - weight = 1.00 - - if weight < 0: - raise QgsProcessingException( - self.tr('Negative weight value found. Please fix your data and try again.')) - - if clazz not in means: - means[clazz] = (0, 0, 0) - - (cx, cy, totalweight) = means[clazz] - geom = QgsGeometry(feat.geometry()) - geom = vector.extractPoints(geom) - for i in geom: - cx += i.x() * weight - cy += i.y() * weight - totalweight += weight - means[clazz] = (cx, cy, totalweight) - - current = 0 - total = 100.0 / len(means) if means else 1 - for (clazz, values) in list(means.items()): - if feedback.isCanceled(): - break - - outFeat = QgsFeature() - cx = values[0] / values[2] - cy = values[1] / values[2] - meanPoint = QgsPointXY(cx, cy) - - outFeat.setGeometry(QgsGeometry.fromPoint(meanPoint)) - attributes = [cx, cy] - if unique_index >= 0: - attributes.append(clazz) - outFeat.setAttributes(attributes) - sink.addFeature(outFeat, QgsFeatureSink.FastInsert) - current += 1 - feedback.setProgress(int(current * total)) - - return {self.OUTPUT: dest_id} diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py index b7dd9d36825..76b3c0bda8e 100755 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -92,7 +92,6 @@ from .ImportIntoSpatialite import ImportIntoSpatialite from .Intersection import Intersection from .JoinAttributes import JoinAttributes from .LinesToPolygons import LinesToPolygons -from .MeanCoords import MeanCoords from .Merge import Merge from .MinimumBoundingGeometry import MinimumBoundingGeometry from .NearestNeighbourAnalysis import NearestNeighbourAnalysis @@ -226,7 +225,6 @@ class QGISAlgorithmProvider(QgsProcessingProvider): Intersection(), JoinAttributes(), LinesToPolygons(), - MeanCoords(), Merge(), MinimumBoundingGeometry(), NearestNeighbourAnalysis(), diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 15e3043eaa7..db782a17b3a 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -2665,7 +2665,7 @@ tests: name: expected/points_along_lines.gml type: vector - - algorithm: qgis:meancoordinates + - algorithm: native:meancoordinates name: standard mean coordinates params: INPUT: @@ -2676,6 +2676,53 @@ tests: name: expected/mean_coordinates.gml type: vector + - algorithm: native:meancoordinates + name: Mean coordinates, multiple grouped + params: + INPUT: + name: points.gml + type: vector + UID: id2 + results: + OUTPUT: + name: expected/mean_coordinates_unique_grouped.gml + type: vector + pk: id2 + compare: + fields: + fid: skip + + - algorithm: native:meancoordinates + name: Mean coordinates, unique field + params: + INPUT: + name: points.gml + type: vector + UID: id + results: + OUTPUT: + name: expected/mean_coordinates_unique_grouped_2.gml + type: vector + pk: id + compare: + fields: + fid: skip + + - algorithm: native:meancoordinates + name: Mean coordinates, weighted + params: + INPUT: + name: points.gml + type: vector + WEIGHT: id + results: + OUTPUT: + name: expected/unique_coordinates_weight.gml + type: vector + compare: + fields: + fid: skip + - algorithm: native:collect name: single part to multipart params: diff --git a/python/plugins/processing/tools/vector.py b/python/plugins/processing/tools/vector.py index f07da0d10e6..aeb00cd6d2f 100755 --- a/python/plugins/processing/tools/vector.py +++ b/python/plugins/processing/tools/vector.py @@ -90,34 +90,6 @@ def values(source, *attributes): return ret -def extractPoints(geom): - points = [] - if geom.type() == QgsWkbTypes.PointGeometry: - if geom.isMultipart(): - points = geom.asMultiPoint() - else: - points.append(geom.asPoint()) - elif geom.type() == QgsWkbTypes.LineGeometry: - if geom.isMultipart(): - lines = geom.asMultiPolyline() - for line in lines: - points.extend(line) - else: - points = geom.asPolyline() - elif geom.type() == QgsWkbTypes.PolygonGeometry: - if geom.isMultipart(): - polygons = geom.asMultiPolygon() - for poly in polygons: - for line in poly: - points.extend(line) - else: - polygon = geom.asPolygon() - for line in polygon: - points.extend(line) - - return points - - def checkMinDistance(point, index, distance, points): """Check if distance from given point to all other points is greater than given value. diff --git a/src/core/processing/qgsnativealgorithms.cpp b/src/core/processing/qgsnativealgorithms.cpp index 29296780f62..1f66ed01ead 100644 --- a/src/core/processing/qgsnativealgorithms.cpp +++ b/src/core/processing/qgsnativealgorithms.cpp @@ -87,6 +87,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsExtentToLayerAlgorithm() ); addAlgorithm( new QgsLineIntersectionAlgorithm() ); addAlgorithm( new QgsSplitWithLinesAlgorithm() ); + addAlgorithm( new QgsMeanCoordinatesAlgorithm() ); } void QgsSaveSelectedFeatures::initAlgorithm( const QVariantMap & ) @@ -2412,6 +2413,179 @@ QVariantMap QgsSplitWithLinesAlgorithm::processAlgorithm( const QVariantMap &par } +void QgsMeanCoordinatesAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), + QObject::tr( "Input layer" ), QList< int >() << QgsProcessing::TypeVectorAnyGeometry ) ); + addParameter( new QgsProcessingParameterField( QStringLiteral( "WEIGHT" ), QObject::tr( "Weight field" ), + QVariant(), QStringLiteral( "INPUT" ), + QgsProcessingParameterField::Numeric, false, true ) ); + addParameter( new QgsProcessingParameterField( QStringLiteral( "UID" ), + QObject::tr( "Unique ID field" ), QVariant(), + QStringLiteral( "INPUT" ), QgsProcessingParameterField::Any, false, true ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Mean coordinates" ), QgsProcessing::TypeVectorPoint ) ); +} + +QString QgsMeanCoordinatesAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm computes a point layer with the center of mass of geometries in an input layer.\n\n" + "An attribute can be specified as containing weights to be applied to each feature when computing the center of mass.\n\n" + "If an attribute is selected in the parameter, features will be grouped according " + "to values in this field. Instead of a single point with the center of mass of the whole layer, " + "the output layer will contain a center of mass for the features in each category." ); +} + +QgsMeanCoordinatesAlgorithm *QgsMeanCoordinatesAlgorithm::createInstance() const +{ + return new QgsMeanCoordinatesAlgorithm(); +} + +QVariantMap QgsMeanCoordinatesAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + std::unique_ptr< QgsFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !source ) + return QVariantMap(); + + QString weightFieldName = parameterAsString( parameters, QStringLiteral( "WEIGHT" ), context ); + QString uniqueFieldName = parameterAsString( parameters, QStringLiteral( "UID" ), context ); + + QgsAttributeList attributes; + int weightIndex = -1; + if ( !weightFieldName.isEmpty() ) + { + weightIndex = source->fields().lookupField( weightFieldName ); + if ( weightIndex >= 0 ) + attributes.append( weightIndex ); + } + + int uniqueFieldIndex = -1; + if ( !uniqueFieldName.isEmpty() ) + { + uniqueFieldIndex = source->fields().lookupField( uniqueFieldName ); + if ( uniqueFieldIndex >= 0 ) + attributes.append( uniqueFieldIndex ); + } + + QgsFields fields; + fields.append( QgsField( QStringLiteral( "MEAN_X" ), QVariant::Double, QString(), 24, 15 ) ); + fields.append( QgsField( QStringLiteral( "MEAN_Y" ), QVariant::Double, QString(), 24, 15 ) ); + if ( uniqueFieldIndex >= 0 ) + { + QgsField uniqueField = source->fields().at( uniqueFieldIndex ); + fields.append( uniqueField ); + } + + QString dest; + std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, dest, fields, + QgsWkbTypes::Point, source->sourceCrs() ) ); + if ( !sink ) + return QVariantMap(); + + QgsFeatureIterator features = source->getFeatures( QgsFeatureRequest().setSubsetOfAttributes( attributes ) ); + + double step = source->featureCount() > 0 ? 50.0 / source->featureCount() : 1; + int i = 0; + QgsFeature feat; + + QHash< QVariant, QList< double > > means; + while ( features.nextFeature( feat ) ) + { + i++; + if ( feedback->isCanceled() ) + { + break; + } + + feedback->setProgress( i * step ); + if ( !feat.hasGeometry() ) + continue; + + + QVariant featureClass; + if ( uniqueFieldIndex >= 0 ) + { + featureClass = feat.attribute( uniqueFieldIndex ); + } + else + { + featureClass = QStringLiteral( "#####singleclass#####" ); + } + + double weight = 1; + if ( weightIndex >= 0 ) + { + bool ok = false; + weight = feat.attribute( weightIndex ).toDouble( &ok ); + if ( !ok ) + weight = 1.0; + } + + if ( weight < 0 ) + { + throw QgsProcessingException( QObject::tr( "Negative weight value found. Please fix your data and try again." ) ); + } + + QList< double > values = means.value( featureClass ); + double cx = 0; + double cy = 0; + double totalWeight = 0; + if ( !values.empty() ) + { + cx = values.at( 0 ); + cy = values.at( 1 ); + totalWeight = values.at( 2 ); + } + + QgsVertexId vid; + QgsPoint pt; + const QgsAbstractGeometry *g = feat.geometry().geometry(); + // NOTE - should this be including the duplicate nodes for closed rings? currently it is, + // but I suspect that the expected behavior would be to NOT include these + while ( g->nextVertex( vid, pt ) ) + { + cx += pt.x() * weight; + cy += pt.y() * weight; + totalWeight += weight; + } + + means[featureClass] = QList< double >() << cx << cy << totalWeight; + } + + i = 0; + step = !means.empty() ? 50.0 / means.count() : 1; + for ( auto it = means.constBegin(); it != means.constEnd(); ++it ) + { + i++; + if ( feedback->isCanceled() ) + { + break; + } + + feedback->setProgress( 50 + i * step ); + if ( qgsDoubleNear( it.value().at( 2 ), 0 ) ) + continue; + + QgsFeature outFeat; + double cx = it.value().at( 0 ) / it.value().at( 2 ); + double cy = it.value().at( 1 ) / it.value().at( 2 ); + + QgsPointXY meanPoint( cx, cy ); + outFeat.setGeometry( QgsGeometry::fromPoint( meanPoint ) ); + + QgsAttributes attributes; + attributes << cx << cy; + if ( uniqueFieldIndex >= 0 ) + attributes.append( it.key() ); + + outFeat.setAttributes( attributes ); + sink->addFeature( outFeat, QgsFeatureSink::FastInsert ); + } + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), dest ); + return outputs; +} + ///@endcond diff --git a/src/core/processing/qgsnativealgorithms.h b/src/core/processing/qgsnativealgorithms.h index df3b8d27f25..ce54743ab6d 100644 --- a/src/core/processing/qgsnativealgorithms.h +++ b/src/core/processing/qgsnativealgorithms.h @@ -786,6 +786,30 @@ class QgsSplitWithLinesAlgorithm : public QgsProcessingAlgorithm }; +/** + * Native mean coordinates algorithm. + */ +class QgsMeanCoordinatesAlgorithm : public QgsProcessingAlgorithm +{ + + public: + + QgsMeanCoordinatesAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override { return QStringLiteral( "meancoordinates" ); } + QString displayName() const override { return QObject::tr( "Mean coordinate(s)" ); } + virtual QStringList tags() const override { return QObject::tr( "mean,average,coordinate" ).split( ',' ); } + QString group() const override { return QObject::tr( "Vector analysis" ); } + QString shortHelpString() const override; + QgsMeanCoordinatesAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + virtual QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + +}; + ///@endcond PRIVATE #endif // QGSNATIVEALGORITHMS_H