Add API to add list of geometries to collections

More efficient then adding one by one, and allows for efficient
transferral of geometries when pared with the new takeGeometries
method.
This commit is contained in:
Nyall Dawson 2024-06-03 11:14:54 +10:00
parent 27a2bcf064
commit b43537680c
28 changed files with 381 additions and 3 deletions

View File

@ -121,6 +121,15 @@ reallocations and memory fragmentation.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
%Docstring
Adds a geometry and takes ownership. Returns ``True`` in case of success.
%End
virtual bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ );
%Docstring
Adds a list of geometries to the collection, transferring ownership to the collection.
Returns ``True`` in case of success.
.. versionadded:: 3.38
%End
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -59,6 +59,8 @@ Returns the curve with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
virtual bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ );
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -79,6 +79,7 @@ Returns the line string with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ ) final;
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -283,6 +283,7 @@ Returns the point with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ ) final;
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );
virtual QgsAbstractGeometry *boundary() const /Factory/;

View File

@ -79,6 +79,7 @@ Returns the polygon with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ ) final;
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -64,6 +64,8 @@ Returns the surface with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
virtual bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ );
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );
virtual QgsAbstractGeometry *boundary() const /Factory/;

View File

@ -121,6 +121,15 @@ reallocations and memory fragmentation.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
%Docstring
Adds a geometry and takes ownership. Returns ``True`` in case of success.
%End
virtual bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ );
%Docstring
Adds a list of geometries to the collection, transferring ownership to the collection.
Returns ``True`` in case of success.
.. versionadded:: 3.38
%End
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -59,6 +59,8 @@ Returns the curve with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
virtual bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ );
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -79,6 +79,7 @@ Returns the line string with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ ) final;
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -283,6 +283,7 @@ Returns the point with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ ) final;
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );
virtual QgsAbstractGeometry *boundary() const /Factory/;

View File

@ -79,6 +79,7 @@ Returns the polygon with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ ) final;
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );

View File

@ -64,6 +64,8 @@ Returns the surface with the specified ``index``.
virtual bool addGeometry( QgsAbstractGeometry *g /Transfer/ );
virtual bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries /Transfer/ );
virtual bool insertGeometry( QgsAbstractGeometry *g /Transfer/, int index );
virtual QgsAbstractGeometry *boundary() const /Factory/;

View File

@ -229,6 +229,13 @@ bool QgsGeometryCollection::addGeometry( QgsAbstractGeometry *g )
return true;
}
bool QgsGeometryCollection::addGeometries( const QVector<QgsAbstractGeometry *> &geometries )
{
mGeometries.append( geometries );
clearCache(); //set bounding box invalid
return true;
}
bool QgsGeometryCollection::insertGeometry( QgsAbstractGeometry *g, int index )
{
if ( !g )

View File

@ -204,6 +204,15 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
//! Adds a geometry and takes ownership. Returns TRUE in case of success.
virtual bool addGeometry( QgsAbstractGeometry *g SIP_TRANSFER );
/**
* Adds a list of geometries to the collection, transferring ownership to the collection.
*
* Returns TRUE in case of success.
*
* \since QGIS 3.38
*/
virtual bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries SIP_TRANSFER );
/**
* Inserts a geometry before a specified index and takes ownership. Returns TRUE in case of success.
* \param g geometry to insert. Ownership is transferred to the collection.

View File

@ -165,6 +165,39 @@ bool QgsMultiCurve::addGeometry( QgsAbstractGeometry *g )
return QgsGeometryCollection::addGeometry( g );
}
bool QgsMultiCurve::addGeometries( const QVector<QgsAbstractGeometry *> &geometries )
{
for ( QgsAbstractGeometry *g : geometries )
{
if ( !qgsgeometry_cast<QgsCurve *>( g ) )
{
qDeleteAll( geometries );
return false;
}
}
if ( mGeometries.empty() && !geometries.empty() )
{
setZMTypeFromSubGeometry( geometries.at( 0 ), Qgis::WkbType::MultiCurve );
}
mGeometries.reserve( mGeometries.size() + geometries.size() );
for ( QgsAbstractGeometry *g : geometries )
{
if ( is3D() && !g->is3D() )
g->addZValue();
else if ( !is3D() && g->is3D() )
g->dropZValue();
if ( isMeasure() && !g->isMeasure() )
g->addMValue();
else if ( !isMeasure() && g->isMeasure() )
g->dropMValue();
mGeometries.append( g );
}
clearCache();
return true;
}
bool QgsMultiCurve::insertGeometry( QgsAbstractGeometry *g, int index )
{
if ( !g || !qgsgeometry_cast<QgsCurve *>( g ) )

View File

@ -83,6 +83,7 @@ class CORE_EXPORT QgsMultiCurve: public QgsGeometryCollection
QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override;
json asJsonObject( int precision = 17 ) const override SIP_SKIP;
bool addGeometry( QgsAbstractGeometry *g SIP_TRANSFER ) override;
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries SIP_TRANSFER ) override;
bool insertGeometry( QgsAbstractGeometry *g SIP_TRANSFER, int index ) override;
/**

View File

@ -180,6 +180,39 @@ bool QgsMultiLineString::addGeometry( QgsAbstractGeometry *g )
return QgsGeometryCollection::addGeometry( g ); // NOLINT(bugprone-parent-virtual-call) clazy:exclude=skipped-base-method
}
bool QgsMultiLineString::addGeometries( const QVector<QgsAbstractGeometry *> &geometries )
{
for ( QgsAbstractGeometry *g : geometries )
{
if ( !qgsgeometry_cast<QgsLineString *>( g ) )
{
qDeleteAll( geometries );
return false;
}
}
if ( mGeometries.empty() && !geometries.empty() )
{
setZMTypeFromSubGeometry( geometries.at( 0 ), Qgis::WkbType::MultiLineString );
}
mGeometries.reserve( mGeometries.size() + geometries.size() );
for ( QgsAbstractGeometry *g : geometries )
{
if ( is3D() && !g->is3D() )
g->addZValue();
else if ( !is3D() && g->is3D() )
g->dropZValue();
if ( isMeasure() && !g->isMeasure() )
g->addMValue();
else if ( !isMeasure() && g->isMeasure() )
g->dropMValue();
mGeometries.append( g );
}
clearCache();
return true;
}
bool QgsMultiLineString::insertGeometry( QgsAbstractGeometry *g, int index )
{
if ( !g || QgsWkbTypes::flatType( g->wkbType() ) != Qgis::WkbType::LineString )

View File

@ -105,6 +105,7 @@ class CORE_EXPORT QgsMultiLineString: public QgsMultiCurve
QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override;
json asJsonObject( int precision = 17 ) const override SIP_SKIP;
bool addGeometry( QgsAbstractGeometry *g SIP_TRANSFER ) override;
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries SIP_TRANSFER ) final;
bool insertGeometry( QgsAbstractGeometry *g SIP_TRANSFER, int index ) override;
/**

View File

@ -248,6 +248,39 @@ bool QgsMultiPoint::addGeometry( QgsAbstractGeometry *g )
return QgsGeometryCollection::addGeometry( g );
}
bool QgsMultiPoint::addGeometries( const QVector<QgsAbstractGeometry *> &geometries )
{
for ( QgsAbstractGeometry *g : geometries )
{
if ( !qgsgeometry_cast<QgsPoint *>( g ) )
{
qDeleteAll( geometries );
return false;
}
}
if ( mGeometries.empty() && !geometries.empty() )
{
setZMTypeFromSubGeometry( geometries.at( 0 ), Qgis::WkbType::MultiPoint );
}
mGeometries.reserve( mGeometries.size() + geometries.size() );
for ( QgsAbstractGeometry *g : geometries )
{
if ( is3D() && !g->is3D() )
g->addZValue();
else if ( !is3D() && g->is3D() )
g->dropZValue();
if ( isMeasure() && !g->isMeasure() )
g->addMValue();
else if ( !isMeasure() && g->isMeasure() )
g->dropMValue();
mGeometries.append( g );
}
clearCache();
return true;
}
bool QgsMultiPoint::insertGeometry( QgsAbstractGeometry *g, int index )
{
if ( !g || QgsWkbTypes::flatType( g->wkbType() ) != Qgis::WkbType::Point )

View File

@ -337,6 +337,7 @@ class CORE_EXPORT QgsMultiPoint: public QgsGeometryCollection
json asJsonObject( int precision = 17 ) const override SIP_SKIP;
int nCoordinates() const override SIP_HOLDGIL;
bool addGeometry( QgsAbstractGeometry *g SIP_TRANSFER ) override;
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries SIP_TRANSFER ) final;
bool insertGeometry( QgsAbstractGeometry *g SIP_TRANSFER, int index ) override;
QgsAbstractGeometry *boundary() const override SIP_FACTORY;
int vertexNumberFromVertexId( QgsVertexId id ) const override;

View File

@ -192,6 +192,39 @@ bool QgsMultiPolygon::addGeometry( QgsAbstractGeometry *g )
return QgsGeometryCollection::addGeometry( g ); // NOLINT(bugprone-parent-virtual-call) clazy:exclude=skipped-base-method
}
bool QgsMultiPolygon::addGeometries( const QVector<QgsAbstractGeometry *> &geometries )
{
for ( QgsAbstractGeometry *g : geometries )
{
if ( !qgsgeometry_cast<QgsPolygon *>( g ) )
{
qDeleteAll( geometries );
return false;
}
}
if ( mGeometries.empty() && !geometries.empty() )
{
setZMTypeFromSubGeometry( geometries.at( 0 ), Qgis::WkbType::MultiPolygon );
}
mGeometries.reserve( mGeometries.size() + geometries.size() );
for ( QgsAbstractGeometry *g : geometries )
{
if ( is3D() && !g->is3D() )
g->addZValue();
else if ( !is3D() && g->is3D() )
g->dropZValue();
if ( isMeasure() && !g->isMeasure() )
g->addMValue();
else if ( !isMeasure() && g->isMeasure() )
g->dropMValue();
mGeometries.append( g );
}
clearCache();
return true;
}
bool QgsMultiPolygon::insertGeometry( QgsAbstractGeometry *g, int index )
{
if ( !g || !qgsgeometry_cast< QgsPolygon * >( g ) )

View File

@ -105,6 +105,7 @@ class CORE_EXPORT QgsMultiPolygon: public QgsMultiSurface
QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override;
json asJsonObject( int precision = 17 ) const override SIP_SKIP;
bool addGeometry( QgsAbstractGeometry *g SIP_TRANSFER ) override;
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries SIP_TRANSFER ) final;
bool insertGeometry( QgsAbstractGeometry *g SIP_TRANSFER, int index ) override;
/**

View File

@ -175,6 +175,39 @@ bool QgsMultiSurface::addGeometry( QgsAbstractGeometry *g )
return QgsGeometryCollection::addGeometry( g );
}
bool QgsMultiSurface::addGeometries( const QVector<QgsAbstractGeometry *> &geometries )
{
for ( QgsAbstractGeometry *g : geometries )
{
if ( !qgsgeometry_cast<QgsSurface *>( g ) )
{
qDeleteAll( geometries );
return false;
}
}
if ( mGeometries.empty() && !geometries.empty() )
{
setZMTypeFromSubGeometry( geometries.at( 0 ), Qgis::WkbType::MultiSurface );
}
mGeometries.reserve( mGeometries.size() + geometries.size() );
for ( QgsAbstractGeometry *g : geometries )
{
if ( is3D() && !g->is3D() )
g->addZValue();
else if ( !is3D() && g->is3D() )
g->dropZValue();
if ( isMeasure() && !g->isMeasure() )
g->addMValue();
else if ( !isMeasure() && g->isMeasure() )
g->dropMValue();
mGeometries.append( g );
}
clearCache();
return true;
}
bool QgsMultiSurface::insertGeometry( QgsAbstractGeometry *g, int index )
{
if ( !g || !qgsgeometry_cast< QgsSurface * >( g ) )

View File

@ -89,6 +89,7 @@ class CORE_EXPORT QgsMultiSurface: public QgsGeometryCollection
QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override;
json asJsonObject( int precision = 17 ) const override SIP_SKIP;
bool addGeometry( QgsAbstractGeometry *g SIP_TRANSFER ) override;
bool addGeometries( const QVector< QgsAbstractGeometry * > &geometries SIP_TRANSFER ) override;
bool insertGeometry( QgsAbstractGeometry *g SIP_TRANSFER, int index ) override;
QgsAbstractGeometry *boundary() const override SIP_FACTORY;

View File

@ -560,6 +560,34 @@ class TestQgsGeometryCollection(QgisTestCase):
'PolygonZ ((11 22 33, 13 14 33, 11 14 33, 11 22 33))']
)
def test_add_geometries(self):
"""
Test adding multiple geometries
"""
# empty collection
collection = QgsGeometryCollection()
self.assertTrue(collection.addGeometries([]))
self.assertEqual(collection.asWkt(), 'GeometryCollection EMPTY')
self.assertEqual(collection.boundingBox(), QgsRectangle())
self.assertTrue(
collection.addGeometries([
QgsLineString([[1, 2, 3], [3, 4, 3], [1, 4, 3], [1, 2, 3]]),
QgsLineString(
[[11, 22, 33], [13, 14, 33], [11, 14, 33], [11, 22, 33]])])
)
self.assertEqual(collection.asWkt(),
'GeometryCollection (LineStringZ (1 2 3, 3 4 3, 1 4 3, 1 2 3),LineStringZ (11 22 33, 13 14 33, 11 14 33, 11 22 33))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 13, 22))
self.assertTrue(
collection.addGeometries([
QgsPoint(100, 200)]
))
self.assertEqual(collection.asWkt(), 'GeometryCollection (LineStringZ (1 2 3, 3 4 3, 1 4 3, 1 2 3),LineStringZ (11 22 33, 13 14 33, 11 14 33, 11 22 33),Point (100 200))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 100, 200))
if __name__ == '__main__':
unittest.main()

View File

@ -11,7 +11,12 @@ __copyright__ = 'Copyright 2023, The QGIS Project'
import qgis # NOQA
from qgis.core import QgsMultiLineString, QgsLineString, QgsPoint
from qgis.core import (
QgsMultiLineString,
QgsLineString,
QgsPoint,
QgsRectangle
)
import unittest
from qgis.testing import start_app, QgisTestCase
@ -189,6 +194,45 @@ class TestQgsMultiLineString(QgisTestCase):
self.assertTrue(geom1.fuzzyEqual(geom2, epsilon))
self.assertTrue(geom1.fuzzyDistanceEqual(geom2, epsilon))
def test_add_geometries(self):
"""
Test adding multiple geometries
"""
# empty collection
collection = QgsMultiLineString()
self.assertTrue(collection.addGeometries([]))
self.assertEqual(collection.asWkt(), 'MultiLineString EMPTY')
self.assertEqual(collection.boundingBox(), QgsRectangle())
self.assertTrue(
collection.addGeometries([
QgsLineString([[1, 2, 3], [3, 4, 3], [1, 4, 3], [1, 2, 3]]),
QgsLineString(
[[11, 22, 33], [13, 14, 33], [11, 14, 33], [11, 22, 33]])])
)
self.assertEqual(collection.asWkt(),
'MultiLineStringZ ((1 2 3, 3 4 3, 1 4 3, 1 2 3),(11 22 33, 13 14 33, 11 14 33, 11 22 33))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 13, 22))
# can't add non-linestrings
self.assertFalse(
collection.addGeometries([
QgsPoint(100, 200)]
))
self.assertEqual(collection.asWkt(),
'MultiLineStringZ ((1 2 3, 3 4 3, 1 4 3, 1 2 3),(11 22 33, 13 14 33, 11 14 33, 11 22 33))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 13, 22))
self.assertTrue(
collection.addGeometries([
QgsLineString([[100, 2, 3], [300, 4, 3]])])
)
self.assertEqual(collection.asWkt(), 'MultiLineStringZ ((1 2 3, 3 4 3, 1 4 3, 1 2 3),(11 22 33, 13 14 33, 11 14 33, 11 22 33),(100 2 3, 300 4 3))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 300, 22))
if __name__ == '__main__':
unittest.main()

View File

@ -11,7 +11,12 @@ __copyright__ = 'Copyright 2023, The QGIS Project'
import qgis # NOQA
from qgis.core import QgsMultiPoint, QgsPoint
from qgis.core import (
QgsMultiPoint,
QgsPoint,
QgsRectangle,
QgsLineString
)
import unittest
from qgis.testing import start_app, QgisTestCase
@ -105,6 +110,44 @@ class TestQgsMultiPoint(QgisTestCase):
self.assertTrue(geom1.fuzzyEqual(geom2, epsilon))
self.assertTrue(geom1.fuzzyDistanceEqual(geom2, epsilon))
def test_add_geometries(self):
"""
Test adding multiple geometries
"""
# empty collection
collection = QgsMultiPoint()
self.assertTrue(collection.addGeometries([]))
self.assertEqual(collection.asWkt(), 'MultiPoint EMPTY')
self.assertEqual(collection.boundingBox(), QgsRectangle())
self.assertTrue(
collection.addGeometries([
QgsPoint(1, 2, 3),
QgsPoint(11, 22, 33)])
)
self.assertEqual(collection.asWkt(),
'MultiPointZ ((1 2 3),(11 22 33))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 11, 22))
# can't add non-points
self.assertFalse(
collection.addGeometries([
QgsLineString([[100, 200], [200, 200]])]
))
self.assertEqual(collection.asWkt(),
'MultiPointZ ((1 2 3),(11 22 33))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 11, 22))
self.assertTrue(
collection.addGeometries([
QgsPoint(100, 2, 3)])
)
self.assertEqual(collection.asWkt(), 'MultiPointZ ((1 2 3),(11 22 33),(100 2 3))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 100, 22))
if __name__ == '__main__':
unittest.main()

View File

@ -11,7 +11,13 @@ __copyright__ = 'Copyright 2023, The QGIS Project'
import qgis # NOQA
from qgis.core import QgsMultiPolygon, QgsPolygon, QgsLineString, QgsPoint
from qgis.core import (
QgsMultiPolygon,
QgsPolygon,
QgsLineString,
QgsPoint,
QgsRectangle
)
import unittest
from qgis.testing import start_app, QgisTestCase
@ -199,6 +205,45 @@ class TestQgsMultiPolygon(QgisTestCase):
self.assertTrue(geom1.fuzzyEqual(geom2, epsilon))
self.assertTrue(geom1.fuzzyDistanceEqual(geom2, epsilon))
def test_add_geometries(self):
"""
Test adding multiple geometries
"""
# empty collection
collection = QgsMultiPolygon()
self.assertTrue(collection.addGeometries([]))
self.assertEqual(collection.asWkt(), 'MultiPolygon EMPTY')
self.assertEqual(collection.boundingBox(), QgsRectangle())
self.assertTrue(
collection.addGeometries([
QgsPolygon(QgsLineString([[1, 2, 3], [3, 4, 3], [1, 4, 3], [1, 2, 3]])),
QgsPolygon(QgsLineString(
[[11, 22, 33], [13, 14, 33], [11, 14, 33], [11, 22, 33]]))])
)
self.assertEqual(collection.asWkt(),
'MultiPolygonZ (((1 2 3, 3 4 3, 1 4 3, 1 2 3)),((11 22 33, 13 14 33, 11 14 33, 11 22 33)))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 13, 22))
# can't add non-polygons
self.assertFalse(
collection.addGeometries([
QgsPoint(100, 200)]
))
self.assertEqual(collection.asWkt(),
'MultiPolygonZ (((1 2 3, 3 4 3, 1 4 3, 1 2 3)),((11 22 33, 13 14 33, 11 14 33, 11 22 33)))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 13, 22))
self.assertTrue(
collection.addGeometries([
QgsPolygon(QgsLineString([[100, 2, 3], [300, 4, 3], [300, 100, 3], [100, 2, 3]]))])
)
self.assertEqual(collection.asWkt(), 'MultiPolygonZ (((1 2 3, 3 4 3, 1 4 3, 1 2 3)),((11 22 33, 13 14 33, 11 14 33, 11 22 33)),((100 2 3, 300 4 3, 300 100 3, 100 2 3)))')
self.assertEqual(collection.boundingBox(),
QgsRectangle(1, 2, 300, 100))
if __name__ == '__main__':
unittest.main()