Skip to content

Commit

Permalink
Merge pull request #3419 from nirvn/aggregate_collect_geom
Browse files Browse the repository at this point in the history
[expression] support collection of geometry in the aggregate() function
  • Loading branch information
nyalldawson committed Aug 24, 2016
2 parents 5d38dcb + 2a326ef commit 014409d
Show file tree
Hide file tree
Showing 12 changed files with 176 additions and 4 deletions.
2 changes: 2 additions & 0 deletions python/core/geometry/qgsgeometry.sip
Expand Up @@ -80,6 +80,8 @@ class QgsGeometry
static QgsGeometry fromMultiPolygon( const QgsMultiPolygon& multipoly );
/** Creates a new geometry from a QgsRectangle */
static QgsGeometry fromRect( const QgsRectangle& rect );
/** Creates a new multipart geometry from a list of QgsGeometry objects*/
static QgsGeometry collectGeometry( const QList< QgsGeometry >& geometries );

/**
* Set the geometry, feeding in a geometry in GEOS format.
Expand Down
1 change: 1 addition & 0 deletions python/core/qgsaggregatecalculator.sip
Expand Up @@ -38,6 +38,7 @@ class QgsAggregateCalculator
StringMinimumLength, //!< Minimum length of string (string fields only)
StringMaximumLength, //!< Maximum length of string (string fields only)
StringConcatenate, //! Concatenate values with a joining string (string fields only). Specify the delimiter using setDelimiter().
GeometryCollect, //! Create a multipart geometry from aggregated geometries
};

//! A bundle of parameters controlling aggregate calculation
Expand Down
2 changes: 1 addition & 1 deletion resources/function_help/json/aggregate
Expand Up @@ -4,7 +4,7 @@
"description": "Returns an aggregate value calculated using features from another layer.",
"arguments": [
{"arg":"layer", "description":"a string, representing either a layer name or layer ID"},
{"arg":"aggregate", "description":"a string corresponding to the aggregate to calculate. Valid options are:<br /><ul><li>count</li><li>count_distinct</li><li>count_missing</li><li>min</li><li>max</li><li>sum</li><li>mean</li><li>median</li><li>stdev</li><li>stdevsample</li><li>range</li><li>minority</li><li>majority</li><li>q1: first quartile</li><li>q3: third quartile</li><li>iqr: inter quartile range</li><li>min_length: minimum string length</li><li>max_length: maximum string length</li><li>concatenate: join strings with a concatenator</li></ul>"},
{"arg":"aggregate", "description":"a string corresponding to the aggregate to calculate. Valid options are:<br /><ul><li>count</li><li>count_distinct</li><li>count_missing</li><li>min</li><li>max</li><li>sum</li><li>mean</li><li>median</li><li>stdev</li><li>stdevsample</li><li>range</li><li>minority</li><li>majority</li><li>q1: first quartile</li><li>q3: third quartile</li><li>iqr: inter quartile range</li><li>min_length: minimum string length</li><li>max_length: maximum string length</li><li>concatenate: join strings with a concatenator</li><li>collect: create an aggregated multipart geometry</li></ul>"},
{"arg":"calculation", "description":"sub expression or field name to aggregate"},
{"arg":"filter", "optional":true, "description":"optional filter expression to limit the features used for calculating the aggregate"},
{"arg":"concatenator", "optional":true, "description":"optional string to use to join values for 'concatenate' aggregate"}
Expand Down
13 changes: 13 additions & 0 deletions resources/function_help/json/collect
@@ -0,0 +1,13 @@
{
"name": "collect",
"type": "function",
"description": "Returns the multipart geometry of aggregated geometries from an expression",
"arguments": [
{"arg":"expression", "description":"geometry expression to aggregate"},
{"arg":"group_by", "optional":true, "description":"optional expression to use to group aggregate calculations"},
{"arg":"filter", "optional":true, "description":"optional expression to use to filter features used to calculate aggregate"}
],
"examples": [
{ "expression":"collect( $geometry )", "returns":"multipart geometry of aggregated geometries"}
]
}
21 changes: 21 additions & 0 deletions src/core/geometry/qgsgeometry.cpp
Expand Up @@ -239,6 +239,27 @@ QgsGeometry QgsGeometry::fromRect( const QgsRectangle& rect )
return fromPolygon( polygon );
}

QgsGeometry QgsGeometry::collectGeometry( const QList< QgsGeometry >& geometries )
{
QgsGeometry collected;

QList< QgsGeometry >::const_iterator git = geometries.constBegin();
for ( ; git != geometries.constEnd(); ++git )
{
if ( collected.isEmpty() )
{
collected = QgsGeometry( *git );
collected.convertToMultiType();
}
else
{
QgsGeometry part = QgsGeometry( *git );
collected.addPart( &part );
}
}
return collected;
}

void QgsGeometry::fromWkb( unsigned char *wkb, int length )
{
detach( false );
Expand Down
2 changes: 2 additions & 0 deletions src/core/geometry/qgsgeometry.h
Expand Up @@ -132,6 +132,8 @@ class CORE_EXPORT QgsGeometry
static QgsGeometry fromMultiPolygon( const QgsMultiPolygon& multipoly );
/** Creates a new geometry from a QgsRectangle */
static QgsGeometry fromRect( const QgsRectangle& rect );
/** Creates a new multipart geometry from a list of QgsGeometry objects*/
static QgsGeometry collectGeometry( const QList< QgsGeometry >& geometries );

/**
* Set the geometry, feeding in a geometry in GEOS format.
Expand Down
40 changes: 40 additions & 0 deletions src/core/qgsaggregatecalculator.cpp
Expand Up @@ -19,6 +19,7 @@
#include "qgsfeature.h"
#include "qgsfeaturerequest.h"
#include "qgsfeatureiterator.h"
#include "qgsgeometry.h"
#include "qgsvectorlayer.h"


Expand Down Expand Up @@ -162,6 +163,8 @@ QgsAggregateCalculator::Aggregate QgsAggregateCalculator::stringToAggregate( con
return StringMaximumLength;
else if ( normalized == "concatenate" )
return StringConcatenate;
else if ( normalized == "collect" )
return GeometryCollect;

if ( ok )
*ok = false;
Expand Down Expand Up @@ -206,6 +209,20 @@ QVariant QgsAggregateCalculator::calculate( QgsAggregateCalculator::Aggregate ag
return calculateDateTimeAggregate( fit, attr, expression, context, stat );
}

case QVariant::UserType:
{
if ( aggregate == GeometryCollect )
{
if ( ok )
*ok = true;
return calculateGeometryAggregate( fit, expression, context );
}
else
{
return QVariant();
}
}

default:
{
// treat as string
Expand Down Expand Up @@ -275,6 +292,7 @@ QgsStatisticalSummary::Statistic QgsAggregateCalculator::numericStatFromAggregat
case StringMinimumLength:
case StringMaximumLength:
case StringConcatenate:
case GeometryCollect:
{
if ( ok )
*ok = false;
Expand Down Expand Up @@ -321,6 +339,7 @@ QgsStringStatisticalSummary::Statistic QgsAggregateCalculator::stringStatFromAgg
case ThirdQuartile:
case InterQuartileRange:
case StringConcatenate:
case GeometryCollect:
{
if ( ok )
*ok = false;
Expand Down Expand Up @@ -366,6 +385,7 @@ QgsDateTimeStatisticalSummary::Statistic QgsAggregateCalculator::dateTimeStatFro
case StringMinimumLength:
case StringMaximumLength:
case StringConcatenate:
case GeometryCollect:
{
if ( ok )
*ok = false;
Expand Down Expand Up @@ -430,6 +450,26 @@ QVariant QgsAggregateCalculator::calculateStringAggregate( QgsFeatureIterator& f
return s.statistic( stat );
}

QVariant QgsAggregateCalculator::calculateGeometryAggregate( QgsFeatureIterator& fit, QgsExpression* expression, QgsExpressionContext* context )
{
Q_ASSERT( expression );

QgsFeature f;
QList< QgsGeometry > geometries;
while ( fit.nextFeature( f ) )
{
Q_ASSERT( context );
context->setFeature( f );
QVariant v = expression->evaluate( context );
if ( v.canConvert<QgsGeometry>() )
{
geometries << v.value<QgsGeometry>();
}
}

return QVariant::fromValue( QgsGeometry::collectGeometry( geometries ) );
}

QVariant QgsAggregateCalculator::concatenateStrings( QgsFeatureIterator& fit, int attr, QgsExpression* expression,
QgsExpressionContext* context, const QString& delimiter )
{
Expand Down
2 changes: 2 additions & 0 deletions src/core/qgsaggregatecalculator.h
Expand Up @@ -64,6 +64,7 @@ class CORE_EXPORT QgsAggregateCalculator
StringMinimumLength, //!< Minimum length of string (string fields only)
StringMaximumLength, //!< Maximum length of string (string fields only)
StringConcatenate, //! Concatenate values with a joining string (string fields only). Specify the delimiter using setDelimiter().
GeometryCollect //! Create a multipart geometry from aggregated geometries
};

//! A bundle of parameters controlling aggregate calculation
Expand Down Expand Up @@ -160,6 +161,7 @@ class CORE_EXPORT QgsAggregateCalculator

static QVariant calculateDateTimeAggregate( QgsFeatureIterator& fit, int attr, QgsExpression* expression,
QgsExpressionContext* context, QgsDateTimeStatisticalSummary::Statistic stat );
static QVariant calculateGeometryAggregate( QgsFeatureIterator& fit, QgsExpression* expression, QgsExpressionContext* context );

static QVariant calculate( Aggregate aggregate, QgsFeatureIterator& fit, QVariant::Type resultType,
int attr, QgsExpression* expression,
Expand Down
10 changes: 8 additions & 2 deletions src/core/qgsexpression.cpp
Expand Up @@ -945,6 +945,11 @@ static QVariant fcnAggregateMaxLength( const QVariantList& values, const QgsExpr
return fcnAggregateGeneric( QgsAggregateCalculator::StringMaximumLength, values, QgsAggregateCalculator::AggregateParameters(), context, parent );
}

static QVariant fcnAggregateCollectGeometry( const QVariantList& values, const QgsExpressionContext* context, QgsExpression *parent )
{
return fcnAggregateGeneric( QgsAggregateCalculator::GeometryCollect, values, QgsAggregateCalculator::AggregateParameters(), context, parent );
}

static QVariant fcnAggregateStringConcat( const QVariantList& values, const QgsExpressionContext* context, QgsExpression *parent )
{
QgsAggregateCalculator::AggregateParameters parameters;
Expand Down Expand Up @@ -3150,7 +3155,7 @@ const QStringList& QgsExpression::BuiltinFunctions()
<< "aggregate" << "relation_aggregate" << "count" << "count_distinct"
<< "count_missing" << "minimum" << "maximum" << "sum" << "mean"
<< "median" << "stdev" << "range" << "minority" << "majority"
<< "q1" << "q3" << "iqr" << "min_length" << "max_length" << "concatenate"
<< "q1" << "q3" << "iqr" << "min_length" << "max_length" << "collect" << "concatenate"
<< "attribute" << "var" << "layer_property"
<< "$id" << "$scale" << "_specialcol_";
}
Expand Down Expand Up @@ -3214,7 +3219,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
<< new StaticFunction( "coalesce", -1, fcnCoalesce, "Conditionals", QString(), false, QStringList(), false, QStringList(), true )
<< new StaticFunction( "if", 3, fcnIf, "Conditionals", QString(), False, QStringList(), true )
<< new StaticFunction( "aggregate", ParameterList() << Parameter( "layer" ) << Parameter( "aggregate" ) << Parameter( "expression" )
<< Parameter( "filter", true ) << Parameter( "concatenator", true ), fcnAggregate, "Aggregates", QString(), False, QStringList(), true )
<< Parameter( "filter", true ) << Parameter( "concatenator", true ), fcnAggregate, "Aggregates", QString(), false, QStringList(), true )
<< new StaticFunction( "relation_aggregate", ParameterList() << Parameter( "relation" ) << Parameter( "aggregate" ) << Parameter( "expression" ) << Parameter( "concatenator", true ),
fcnAggregateRelation, "Aggregates", QString(), False, QStringList( QgsFeatureRequest::AllAttributes ), true )

Expand All @@ -3235,6 +3240,7 @@ const QList<QgsExpression::Function*>& QgsExpression::Functions()
<< new StaticFunction( "iqr", aggParams, fcnAggregateIQR, "Aggregates", QString(), False, QStringList(), true )
<< new StaticFunction( "min_length", aggParams, fcnAggregateMinLength, "Aggregates", QString(), False, QStringList(), true )
<< new StaticFunction( "max_length", aggParams, fcnAggregateMaxLength, "Aggregates", QString(), False, QStringList(), true )
<< new StaticFunction( "collect", aggParams, fcnAggregateCollectGeometry, "Aggregates", QString(), False, QStringList(), true )
<< new StaticFunction( "concatenate", aggParams << Parameter( "concatenator", true ), fcnAggregateStringConcat, "Aggregates", QString(), False, QStringList(), true )

<< new StaticFunction( "regexp_match", 2, fcnRegexpMatch, "Conditionals" )
Expand Down
10 changes: 10 additions & 0 deletions tests/src/core/testqgsexpression.cpp
Expand Up @@ -116,26 +116,32 @@ class TestQgsExpression: public QObject
mAggregatesLayer = new QgsVectorLayer( "Point?field=col1:integer&field=col2:string&field=col3:integer", "aggregate_layer", "memory" );
QVERIFY( mAggregatesLayer->isValid() );
QgsFeature af1( mAggregatesLayer->dataProvider()->fields(), 1 );
af1.setGeometry( QgsGeometry::fromPoint( QgsPoint( 0, 0 ) ) );
af1.setAttribute( "col1", 4 );
af1.setAttribute( "col2", "test" );
af1.setAttribute( "col3", 2 );
QgsFeature af2( mAggregatesLayer->dataProvider()->fields(), 2 );
af2.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1, 0 ) ) );
af2.setAttribute( "col1", 2 );
af2.setAttribute( "col2", QVariant( QVariant::String ) );
af2.setAttribute( "col3", 1 );
QgsFeature af3( mAggregatesLayer->dataProvider()->fields(), 3 );
af3.setGeometry( QgsGeometry::fromPoint( QgsPoint( 2, 0 ) ) );
af3.setAttribute( "col1", 3 );
af3.setAttribute( "col2", "test333" );
af3.setAttribute( "col3", 2 );
QgsFeature af4( mAggregatesLayer->dataProvider()->fields(), 4 );
af4.setGeometry( QgsGeometry::fromPoint( QgsPoint( 3, 0 ) ) );
af4.setAttribute( "col1", 2 );
af4.setAttribute( "col2", "test4" );
af4.setAttribute( "col3", 2 );
QgsFeature af5( mAggregatesLayer->dataProvider()->fields(), 5 );
af5.setGeometry( QgsGeometry::fromPoint( QgsPoint( 4, 0 ) ) );
af5.setAttribute( "col1", 5 );
af5.setAttribute( "col2", QVariant( QVariant::String ) );
af5.setAttribute( "col3", 3 );
QgsFeature af6( mAggregatesLayer->dataProvider()->fields(), 6 );
af6.setGeometry( QgsGeometry::fromPoint( QgsPoint( 5, 0 ) ) );
af6.setAttribute( "col1", 8 );
af6.setAttribute( "col2", "test4" );
af6.setAttribute( "col3", 3 );
Expand Down Expand Up @@ -1213,6 +1219,8 @@ class TestQgsExpression: public QObject
QTest::newRow( "string aggregate 2" ) << "aggregate('test','min_length',\"col2\")" << false << QVariant( 5 );
QTest::newRow( "string concatenate" ) << "aggregate('test','concatenate',\"col2\",concatenator:=' , ')" << false << QVariant( "test1 , test2 , test3 , test4" );

QTest::newRow( "geometry collect" ) << "geom_to_wkt(aggregate('aggregate_layer','collect',$geometry))" << false << QVariant( QString( "MultiPoint ((0 0),(1 0),(2 0),(3 0),(4 0),(5 0))" ) );

QTest::newRow( "sub expression" ) << "aggregate('test','sum',\"col1\" * 2)" << false << QVariant( 65 * 2 );
QTest::newRow( "bad sub expression" ) << "aggregate('test','sum',\"xcvxcv\" * 2)" << true << QVariant();

Expand Down Expand Up @@ -1289,6 +1297,8 @@ class TestQgsExpression: public QObject
QTest::newRow( "max_length" ) << "max_length(\"col2\")" << false << QVariant( 7 );
QTest::newRow( "concatenate" ) << "concatenate(\"col2\",concatenator:=',')" << false << QVariant( "test,,test333,test4,,test4" );

QTest::newRow( "geometry collect" ) << "geom_to_wkt(collect($geometry))" << false << QVariant( QString( "MultiPoint ((0 0),(1 0),(2 0),(3 0),(4 0),(5 0))" ) );

QTest::newRow( "bad expression" ) << "sum(\"xcvxcvcol1\")" << true << QVariant();
QTest::newRow( "aggregate named" ) << "sum(expression:=\"col1\")" << false << QVariant( 24.0 );
QTest::newRow( "string aggregate on int" ) << "max_length(\"col1\")" << true << QVariant();
Expand Down
38 changes: 37 additions & 1 deletion tests/src/python/test_qgsaggregatecalculator.py
Expand Up @@ -21,11 +21,16 @@
QgsInterval,
QgsExpressionContext,
QgsExpressionContextScope,
QgsGeometry,
NULL
)
from qgis.PyQt.QtCore import QDateTime, QDate, QTime
from qgis.testing import unittest, start_app

from utilities import(
compareWkt
)

start_app()


Expand Down Expand Up @@ -55,6 +60,31 @@ def testParameters(self):
self.assertEqual(a.filter(), 'string filter')
self.assertEqual(a.delimiter(), 'delim')

def testGeometry(self):
""" Test calculation of aggregates on geometry expressions """

layer = QgsVectorLayer("Point?",
"layer", "memory")
pr = layer.dataProvider()

# must be same length:
geometry_values = [QgsGeometry.fromWkt("Point ( 0 0 )"), QgsGeometry.fromWkt("Point ( 1 1 )"), QgsGeometry.fromWkt("Point ( 2 2 )")]

features = []
for i in range(len(geometry_values)):
f = QgsFeature()
f.setGeometry(geometry_values[i])
features.append(f)
self.assertTrue(pr.addFeatures(features))

agg = QgsAggregateCalculator(layer)

val, ok = agg.calculate(QgsAggregateCalculator.GeometryCollect, '$geometry')
self.assertTrue(ok)
expwkt = "MultiPoint ((0 0), (1 1), (2 2))"
wkt = val.exportToWkt()
self.assertTrue(compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt))

def testNumeric(self):
""" Test calculation of aggregates on numeric fields"""

Expand Down Expand Up @@ -324,6 +354,11 @@ def testExpression(self):
self.assertTrue(ok)
self.assertEqual(val, '8 oranges')

# geometry
val, ok = agg.calculate(QgsAggregateCalculator.GeometryCollect, "make_point( coalesce(fldint,0), 2 )")
self.assertTrue(ok)
self.assertTrue(val.exportToWkt(), 'MultiPoint((4 2, 2 2, 3 2, 2 2,5 2, 0 2,8 2))')

# try a bad expression
val, ok = agg.calculate(QgsAggregateCalculator.Max, "not_a_field || ' oranges'")
self.assertFalse(ok)
Expand Down Expand Up @@ -372,7 +407,8 @@ def testStringToAggregate(self):
[QgsAggregateCalculator.InterQuartileRange, 'iqr'],
[QgsAggregateCalculator.StringMinimumLength, 'min_length'],
[QgsAggregateCalculator.StringMaximumLength, 'max_length'],
[QgsAggregateCalculator.StringConcatenate, 'concatenate']]
[QgsAggregateCalculator.StringConcatenate, 'concatenate'],
[QgsAggregateCalculator.GeometryCollect, 'collect']]

for t in tests:
agg, ok = QgsAggregateCalculator.stringToAggregate(t[1])
Expand Down
39 changes: 39 additions & 0 deletions tests/src/python/test_qgsgeometry.py
Expand Up @@ -1346,6 +1346,45 @@ def testBoundingBox(self):
line = QgsGeometry.fromPolyline(points)
assert line.boundingBox().isNull()

def testCollectGeometry(self):
# collect points
geometries = [QgsGeometry.fromPoint(QgsPoint(0, 0)), QgsGeometry.fromPoint(QgsPoint(1, 1))]
geometry = QgsGeometry.collectGeometry(geometries)
expwkt = "MultiPoint ((0 0), (1 1))"
wkt = geometry.exportToWkt()
assert compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt)

# collect lines
points = [
[QgsPoint(0, 0), QgsPoint(1, 0)],
[QgsPoint(2, 0), QgsPoint(3, 0)]
]
geometries = [QgsGeometry.fromPolyline(points[0]), QgsGeometry.fromPolyline(points[1])]
geometry = QgsGeometry.collectGeometry(geometries)
expwkt = "MultiLineString ((0 0, 1 0), (2 0, 3 0))"
wkt = geometry.exportToWkt()
assert compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt)

# collect polygons
points = [
[[QgsPoint(0, 0), QgsPoint(1, 0), QgsPoint(1, 1), QgsPoint(0, 1), QgsPoint(0, 0)]],
[[QgsPoint(2, 0), QgsPoint(3, 0), QgsPoint(3, 1), QgsPoint(2, 1), QgsPoint(2, 0)]]
]
geometries = [QgsGeometry.fromPolygon(points[0]), QgsGeometry.fromPolygon(points[1])]
geometry = QgsGeometry.collectGeometry(geometries)
expwkt = "MultiPolygon (((0 0, 1 0, 1 1, 0 1, 0 0)),((2 0, 3 0, 3 1, 2 1, 2 0)))"
wkt = geometry.exportToWkt()
assert compareWkt(expwkt, wkt), "Expected:\n%s\nGot:\n%s\n" % (expwkt, wkt)

# test empty list
geometries = []
geometry = QgsGeometry.collectGeometry(geometries)
assert geometry.isEmpty(), "Expected geometry to be empty"

# check that the resulting geometry is multi
geometry = QgsGeometry.collectGeometry([QgsGeometry.fromWkt('Point (0 0)')])
assert geometry.isMultipart(), "Expected collected geometry to be multipart"

def testAddPart(self):
# add a part to a multipoint
points = [QgsPoint(0, 0), QgsPoint(1, 0)]
Expand Down

0 comments on commit 014409d

Please sign in to comment.