diff --git a/python/core/qgsaggregatecalculator.sip b/python/core/qgsaggregatecalculator.sip index c61e12a9f00..7a340fe7d56 100644 --- a/python/core/qgsaggregatecalculator.sip +++ b/python/core/qgsaggregatecalculator.sip @@ -1,7 +1,9 @@ /** \ingroup core * \class QgsAggregateCalculator * \brief Utility class for calculating aggregates for a field (or expression) over the features - * from a vector layer. + * from a vector layer. It is recommended that QgsVectorLayer::aggregate() is used rather then + * directly using this class, as the QgsVectorLayer method can handle delegating aggregate calculation + * to a data provider for remote calculation. * \note added in QGIS 2.16 */ class QgsAggregateCalculator diff --git a/python/core/qgsvectordataprovider.sip b/python/core/qgsvectordataprovider.sip index 097855508a1..3c917394a72 100644 --- a/python/core/qgsvectordataprovider.sip +++ b/python/core/qgsvectordataprovider.sip @@ -154,6 +154,23 @@ class QgsVectorDataProvider : QgsDataProvider */ virtual void uniqueValues( int index, QList &uniqueValues /Out/, int limit = -1 ); + /** Calculates an aggregated value from the layer's features. The base implementation does nothing, + * but subclasses can override this method to handoff calculation of aggregates to the provider. + * @param aggregate aggregate to calculate + * @param index the index of the attribute to calculate aggregate over + * @param filter optional filter for calculating aggregate over a subset of features, or an + * empty string to use all features + * @param context expression context for filter + * @param ok will be set to true if calculation was successfully performed by the data provider + * @return calculated aggregate value + * @note added in QGIS 2.16 + */ + virtual QVariant aggregate( QgsAggregateCalculator::Aggregate aggregate, + int index, + const QString& filter, + QgsExpressionContext* context, + bool& ok ); + /** * Returns the possible enum values of an attribute. Returns an empty stringlist if a provider does not support enum types * or if the given attribute is not an enum type. diff --git a/python/core/qgsvectorlayer.sip b/python/core/qgsvectorlayer.sip index adea6d0eaa5..b521e76d63b 100644 --- a/python/core/qgsvectorlayer.sip +++ b/python/core/qgsvectorlayer.sip @@ -1275,6 +1275,22 @@ class QgsVectorLayer : QgsMapLayer /** Returns maximum value for an attribute column or invalid variant in case of error */ QVariant maximumValue( int index ); + /** Calculates an aggregated value from the layer's features. + * @param aggregate aggregate to calculate + * @param fieldOrExpression source field or expression to use as basis for aggregated values. + * @param filter optional filter for calculating aggregate over a subset of features, or an + * empty string to use all features + * @param context expression context for expressions and filters + * @param ok if specified, will be set to true if aggregate calculation was successful + * @return calculated aggregate value + * @note added in QGIS 2.16 + */ + QVariant aggregate( QgsAggregateCalculator::Aggregate aggregate, + const QString& fieldOrExpression, + const QString& filter = QString(), + QgsExpressionContext* context = nullptr, + bool* ok = nullptr ); + /** Fetches all values from a specified field name or expression. * @param fieldOrExpression field name or an expression string * @param ok will be set to false if field or expression is invalid, otherwise true diff --git a/src/core/qgsaggregatecalculator.cpp b/src/core/qgsaggregatecalculator.cpp index b661f5c4643..22a531b7743 100644 --- a/src/core/qgsaggregatecalculator.cpp +++ b/src/core/qgsaggregatecalculator.cpp @@ -101,8 +101,16 @@ QVariant QgsAggregateCalculator::calculate( QgsAggregateCalculator::Aggregate ag resultType = mLayer->fields().at( attrNum ).type(); } - QgsFeatureIterator fit = mLayer->getFeatures( request ); + return calculate( aggregate, fit, resultType, attrNum, expression.data(), context, ok ); +} + +QVariant QgsAggregateCalculator::calculate( QgsAggregateCalculator::Aggregate aggregate, QgsFeatureIterator& fit, QVariant::Type resultType, + int attr, QgsExpression* expression, QgsExpressionContext* context, bool* ok ) +{ + if ( ok ) + *ok = false; + switch ( resultType ) { case QVariant::Int: @@ -111,32 +119,40 @@ QVariant QgsAggregateCalculator::calculate( QgsAggregateCalculator::Aggregate ag case QVariant::ULongLong: case QVariant::Double: { - QgsStatisticalSummary::Statistic stat = numericStatFromAggregate( aggregate, ok ); - if ( !ok ) + bool statOk = false; + QgsStatisticalSummary::Statistic stat = numericStatFromAggregate( aggregate, &statOk ); + if ( !statOk ) return QVariant(); - - return calculateNumericAggregate( fit, attrNum, expression.data(), context, stat ); + if ( ok ) + *ok = true; + return calculateNumericAggregate( fit, attr, expression, context, stat ); } case QVariant::Date: case QVariant::DateTime: { - QgsDateTimeStatisticalSummary::Statistic stat = dateTimeStatFromAggregate( aggregate, ok ); - if ( !ok ) + bool statOk = false; + QgsDateTimeStatisticalSummary::Statistic stat = dateTimeStatFromAggregate( aggregate, &statOk ); + if ( !statOk ) return QVariant(); - return calculateDateTimeAggregate( fit, attrNum, expression.data(), context, stat ); + if ( ok ) + *ok = true; + return calculateDateTimeAggregate( fit, attr, expression, context, stat ); } default: { // treat as string - QgsStringStatisticalSummary::Statistic stat = stringStatFromAggregate( aggregate, ok ); - if ( !ok ) + bool statOk = false; + QgsStringStatisticalSummary::Statistic stat = stringStatFromAggregate( aggregate, &statOk ); + if ( !statOk ) return QVariant(); - return calculateStringAggregate( fit, attrNum, expression.data(), context, stat ); + if ( ok ) + *ok = true; + return calculateStringAggregate( fit, attr, expression, context, stat ); } } diff --git a/src/core/qgsaggregatecalculator.h b/src/core/qgsaggregatecalculator.h index cbca70637db..bf1cfa072f9 100644 --- a/src/core/qgsaggregatecalculator.h +++ b/src/core/qgsaggregatecalculator.h @@ -21,6 +21,8 @@ #include "qgsstatisticalsummary.h" #include "qgsdatetimestatisticalsummary.h" #include "qgsstringstatisticalsummary.h" +#include + class QgsFeatureIterator; class QgsExpression; @@ -30,7 +32,9 @@ class QgsExpressionContext; /** \ingroup core * \class QgsAggregateCalculator * \brief Utility class for calculating aggregates for a field (or expression) over the features - * from a vector layer. + * from a vector layer. It is recommended that QgsVectorLayer::aggregate() is used rather then + * directly using this class, as the QgsVectorLayer method can handle delegating aggregate calculation + * to a data provider for remote calculation. * \note added in QGIS 2.16 */ class CORE_EXPORT QgsAggregateCalculator @@ -114,6 +118,10 @@ class CORE_EXPORT QgsAggregateCalculator QgsExpressionContext* context, QgsDateTimeStatisticalSummary::Statistic stat ); QgsExpressionContext* createContext() const; + + static QVariant calculate( Aggregate aggregate, QgsFeatureIterator& fit, QVariant::Type resultType, + int attr, QgsExpression* expression, + QgsExpressionContext* context, bool* ok = nullptr ); }; #endif //QGSAGGREGATECALCULATOR_H diff --git a/src/core/qgsstatisticalsummary.h b/src/core/qgsstatisticalsummary.h index e4edaf90ec2..e0bf0acaff1 100644 --- a/src/core/qgsstatisticalsummary.h +++ b/src/core/qgsstatisticalsummary.h @@ -144,6 +144,7 @@ class CORE_EXPORT QgsStatisticalSummary int count() const { return mCount; } /** Returns the number of missing (null) values + * @note added in QGIS 2.16 */ int countMissing() const { return mMissing; } diff --git a/src/core/qgsvectordataprovider.cpp b/src/core/qgsvectordataprovider.cpp index 0b90dcb6f6e..aeffa093b48 100644 --- a/src/core/qgsvectordataprovider.cpp +++ b/src/core/qgsvectordataprovider.cpp @@ -381,6 +381,19 @@ void QgsVectorDataProvider::uniqueValues( int index, QList &values, in } } +QVariant QgsVectorDataProvider::aggregate( QgsAggregateCalculator::Aggregate aggregate, int index, + const QString& filter, QgsExpressionContext* context, bool& ok ) +{ + //base implementation does nothing + Q_UNUSED( aggregate ); + Q_UNUSED( index ); + Q_UNUSED( filter ); + Q_UNUSED( context ); + + ok = false; + return QVariant(); +} + void QgsVectorDataProvider::clearMinMaxCache() { mCacheMinMaxDirty = true; diff --git a/src/core/qgsvectordataprovider.h b/src/core/qgsvectordataprovider.h index 31b947bb1bf..249ecab7835 100644 --- a/src/core/qgsvectordataprovider.h +++ b/src/core/qgsvectordataprovider.h @@ -28,6 +28,7 @@ class QTextCodec; #include "qgsfeature.h" #include "qgsfield.h" #include "qgsrectangle.h" +#include "qgsaggregatecalculator.h" typedef QList QgsAttributeList; typedef QSet QgsAttributeIds; @@ -202,6 +203,23 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider */ virtual void uniqueValues( int index, QList &uniqueValues, int limit = -1 ); + /** Calculates an aggregated value from the layer's features. The base implementation does nothing, + * but subclasses can override this method to handoff calculation of aggregates to the provider. + * @param aggregate aggregate to calculate + * @param index the index of the attribute to calculate aggregate over + * @param filter optional filter for calculating aggregate over a subset of features, or an + * empty string to use all features + * @param context expression context for filter + * @param ok will be set to true if calculation was successfully performed by the data provider + * @return calculated aggregate value + * @note added in QGIS 2.16 + */ + virtual QVariant aggregate( QgsAggregateCalculator::Aggregate aggregate, + int index, + const QString& filter, + QgsExpressionContext* context, + bool& ok ); + /** * Returns the possible enum values of an attribute. Returns an empty stringlist if a provider does not support enum types * or if the given attribute is not an enum type. diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp index b38838b9ad8..9a49b839a86 100644 --- a/src/core/qgsvectorlayer.cpp +++ b/src/core/qgsvectorlayer.cpp @@ -3254,6 +3254,44 @@ QVariant QgsVectorLayer::maximumValue( int index ) return QVariant(); } +QVariant QgsVectorLayer::aggregate( QgsAggregateCalculator::Aggregate aggregate, const QString& fieldOrExpression, + const QString& filter, QgsExpressionContext* context, bool* ok ) +{ + if ( ok ) + *ok = false; + + if ( !mDataProvider ) + { + return QVariant(); + } + + // test if we are calculating based on a field + int attrIndex = mUpdatedFields.fieldNameIndex( fieldOrExpression ); + if ( attrIndex >= 0 ) + { + // aggregate is based on a field - if it's a provider field, we could possibly hand over the calculation + // to the provider itself + QgsFields::FieldOrigin origin = mUpdatedFields.fieldOrigin( attrIndex ); + if ( origin == QgsFields::OriginProvider ) + { + bool providerOk = false; + QVariant val = mDataProvider->aggregate( aggregate, attrIndex, filter, context, providerOk ); + if ( providerOk ) + { + // provider handled calculation + if ( ok ) + *ok = true; + return val; + } + } + } + + // fallback to using aggregate calculator to determine aggregate + QgsAggregateCalculator c( this ); + c.setFilter( filter ); + return c.calculate( aggregate, fieldOrExpression, context, ok ); +} + QList QgsVectorLayer::getValues( const QString &fieldOrExpression, bool& ok, bool selectedOnly ) { QList values; diff --git a/src/core/qgsvectorlayer.h b/src/core/qgsvectorlayer.h index d98023ebbbe..c9ba3c2784a 100644 --- a/src/core/qgsvectorlayer.h +++ b/src/core/qgsvectorlayer.h @@ -35,6 +35,7 @@ #include "qgsvectorsimplifymethod.h" #include "qgseditformconfig.h" #include "qgsattributetableconfig.h" +#include "qgsaggregatecalculator.h" class QPainter; class QImage; @@ -1656,6 +1657,22 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer /** Returns maximum value for an attribute column or invalid variant in case of error */ QVariant maximumValue( int index ); + /** Calculates an aggregated value from the layer's features. + * @param aggregate aggregate to calculate + * @param fieldOrExpression source field or expression to use as basis for aggregated values. + * @param filter optional filter for calculating aggregate over a subset of features, or an + * empty string to use all features + * @param context expression context for expressions and filters + * @param ok if specified, will be set to true if aggregate calculation was successful + * @return calculated aggregate value + * @note added in QGIS 2.16 + */ + QVariant aggregate( QgsAggregateCalculator::Aggregate aggregate, + const QString& fieldOrExpression, + const QString& filter = QString(), + QgsExpressionContext* context = nullptr, + bool* ok = nullptr ); + /** Fetches all values from a specified field name or expression. * @param fieldOrExpression field name or an expression string * @param ok will be set to false if field or expression is invalid, otherwise true diff --git a/tests/src/python/test_qgsvectorlayer.py b/tests/src/python/test_qgsvectorlayer.py index e3e65d0beab..de0181b1858 100644 --- a/tests/src/python/test_qgsvectorlayer.py +++ b/tests/src/python/test_qgsvectorlayer.py @@ -33,7 +33,8 @@ from qgis.core import (QGis, QgsSingleSymbolRendererV2, QgsCoordinateReferenceSystem, QgsProject, - QgsUnitTypes) + QgsUnitTypes, + QgsAggregateCalculator) from qgis.testing import start_app, unittest from utilities import unitTestDataPath start_app() @@ -1062,6 +1063,44 @@ class TestQgsVectorLayer(unittest.TestCase): assert(len(list(features)) == 1) + def testAggregate(self): + """ Test aggregate calculation """ + layer = QgsVectorLayer("Point?field=fldint:integer", "layer", "memory") + pr = layer.dataProvider() + + int_values = [4, 2, 3, 2, 5, None, 8] + features = [] + for i in int_values: + f = QgsFeature() + f.setFields(layer.fields()) + f.setAttributes([i]) + features.append(f) + assert pr.addFeatures(features) + + tests = [[QgsAggregateCalculator.Count, 6], + [QgsAggregateCalculator.Sum, 24], + [QgsAggregateCalculator.Mean, 4], + [QgsAggregateCalculator.StDev, 2.0816], + [QgsAggregateCalculator.StDevSample, 2.2803], + [QgsAggregateCalculator.Min, 2], + [QgsAggregateCalculator.Max, 8], + [QgsAggregateCalculator.Range, 6], + [QgsAggregateCalculator.Median, 3.5], + [QgsAggregateCalculator.CountDistinct, 5], + [QgsAggregateCalculator.CountMissing, 1], + [QgsAggregateCalculator.FirstQuartile, 2], + [QgsAggregateCalculator.ThirdQuartile, 5.0], + [QgsAggregateCalculator.InterQuartileRange, 3.0] + ] + + for t in tests: + val, ok = layer.aggregate(t[0], 'fldint') + self.assertTrue(ok) + if isinstance(t[1], int): + self.assertEqual(val, t[1]) + else: + self.assertAlmostEqual(val, t[1], 3) + def onLayerTransparencyChanged(self, tr): self.transparencyTest = tr