Skip to content

Commit

Permalink
[FEATURE][API] Add some nice PyQGIS API for working with geometry col…
Browse files Browse the repository at this point in the history
…lections

- Calling removeGeometry with an invalid index will now raise an IndexError
- Calling collection[0] will return the first geometry in the collection,
collection[1] the second, etc. And negative indices return from the end
of the collection, so collection[-1] returns the last geometry in the collection.
- Geometries can be deleted by calling `del collection[1]` (deletes the
second geometry from the collection). Also supports negative indices
to count from the end of the collection.
  • Loading branch information
nyalldawson committed Dec 6, 2018
1 parent 31b82de commit 4bba8ae
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 2 deletions.
67 changes: 65 additions & 2 deletions python/core/auto_generated/geometry/qgsgeometrycollection.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,26 @@ Inserts a geometry before a specified index and takes ownership. Returns true in
:param index: position to insert geometry before
%End


virtual bool removeGeometry( int nr );
%Docstring
Removes a geometry from the collection.
Removes a geometry from the collection by index.

:param nr: index of geometry to remove
An IndexError will be raised if no geometry with the specified index exists.

:return: true if removal was successful.
%End
%MethodCode
const int count = sipCpp->numGeometries();
if ( a0 < 0 || a0 >= count )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->removeGeometry( a0 );
}
%End

virtual void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) throw( QgsCsException );
Expand Down Expand Up @@ -207,6 +220,56 @@ Returns a geometry without curves. Caller takes ownership





SIP_PYOBJECT __getitem__( int index );
%Docstring
Returns the geometry at the specified ``index``. An IndexError will be raised if no geometry with the specified ``index`` exists.

Indexes can be less than 0, in which case they correspond to geometries from the end of the collect. E.g. an index of -1
corresponds to the last geometry in the collection.

.. versionadded:: 3.6
%End
%MethodCode
const int count = sipCpp->numGeometries();
if ( a0 < -count || a0 >= count )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else if ( a0 >= 0 )
{
return sipConvertFromType( sipCpp->geometryN( a0 ), sipType_QgsAbstractGeometry, NULL );
}
else
{
return sipConvertFromType( sipCpp->geometryN( count + a0 ), sipType_QgsAbstractGeometry, NULL );
}
%End

void __delitem__( int index );
%Docstring
Deletes the geometry at the specified ``index``. A geometry at the ``index`` must already exist or an IndexError will be raised.

Indexes can be less than 0, in which case they correspond to geometries from the end of the collection. E.g. an index of -1
corresponds to the last geometry in the collection.

.. versionadded:: 3.6
%End
%MethodCode
const int count = sipCpp->numGeometries();
if ( a0 >= 0 && a0 < count )
sipCpp->removeGeometry( a0 );
else if ( a0 < 0 && a0 >= -count )
sipCpp->removeGeometry( count + a0 );
else
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
%End

virtual QgsGeometryCollection *createEmptyWithSameType() const /Factory/;


Expand Down
77 changes: 77 additions & 0 deletions src/core/geometry/qgsgeometrycollection.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,37 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
*/
virtual bool insertGeometry( QgsAbstractGeometry *g SIP_TRANSFER, int index );

#ifndef SIP_RUN

/**
* Removes a geometry from the collection.
* \param nr index of geometry to remove
* \returns true if removal was successful.
*/
virtual bool removeGeometry( int nr );
#else

/**
* Removes a geometry from the collection by index.
*
* An IndexError will be raised if no geometry with the specified index exists.
*
* \returns true if removal was successful.
*/
virtual bool removeGeometry( int nr );
% MethodCode
const int count = sipCpp->numGeometries();
if ( a0 < 0 || a0 >= count )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->removeGeometry( a0 );
}
% End
#endif

void transform( const QgsCoordinateTransform &ct, QgsCoordinateTransform::TransformDirection d = QgsCoordinateTransform::ForwardTransform, bool transformZ = false ) override SIP_THROW( QgsCsException );
void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;
Expand Down Expand Up @@ -202,6 +227,58 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry
}
#endif


#ifdef SIP_RUN

/**
* Returns the geometry at the specified ``index``. An IndexError will be raised if no geometry with the specified ``index`` exists.
*
* Indexes can be less than 0, in which case they correspond to geometries from the end of the collect. E.g. an index of -1
* corresponds to the last geometry in the collection.
*
* \since QGIS 3.6
*/
SIP_PYOBJECT __getitem__( int index );
% MethodCode
const int count = sipCpp->numGeometries();
if ( a0 < -count || a0 >= count )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else if ( a0 >= 0 )
{
return sipConvertFromType( sipCpp->geometryN( a0 ), sipType_QgsAbstractGeometry, NULL );
}
else
{
return sipConvertFromType( sipCpp->geometryN( count + a0 ), sipType_QgsAbstractGeometry, NULL );
}
% End

/**
* Deletes the geometry at the specified ``index``. A geometry at the ``index`` must already exist or an IndexError will be raised.
*
* Indexes can be less than 0, in which case they correspond to geometries from the end of the collection. E.g. an index of -1
* corresponds to the last geometry in the collection.
*
* \since QGIS 3.6
*/
void __delitem__( int index );
% MethodCode
const int count = sipCpp->numGeometries();
if ( a0 >= 0 && a0 < count )
sipCpp->removeGeometry( a0 );
else if ( a0 < 0 && a0 >= -count )
sipCpp->removeGeometry( count + a0 );
else
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
% End
#endif

QgsGeometryCollection *createEmptyWithSameType() const override SIP_FACTORY;

protected:
Expand Down
65 changes: 65 additions & 0 deletions tests/src/python/test_qgsgeometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,71 @@ def testLineStringPythonAdditions(self):
with self.assertRaises(IndexError):
del ls[-3]

def testGeometryCollectionPythonAdditions(self):
"""
Tests Python specific additions to the QgsGeometryCollection API
"""
g = QgsGeometryCollection()
self.assertTrue(bool(g))
self.assertEqual(len(g), 0)
g = QgsGeometryCollection()
g.fromWkt('GeometryCollection( Point(1 2), Point(11 12))')
self.assertTrue(bool(g))
self.assertEqual(len(g), 2)

# pointN
with self.assertRaises(IndexError):
g.geometryN(-1)
with self.assertRaises(IndexError):
g.geometryN(2)
self.assertEqual(g.geometryN(0), QgsPoint(1, 2))
self.assertEqual(g.geometryN(1), QgsPoint(11, 12))

# removeGeometry
g.fromWkt('GeometryCollection( Point(1 2), Point(11 12), Point(33 34))')
with self.assertRaises(IndexError):
g.removeGeometry(-1)
with self.assertRaises(IndexError):
g.removeGeometry(3)
g.removeGeometry(1)
self.assertEqual(len(g), 2)
self.assertEqual(g.geometryN(0), QgsPoint(1, 2))
self.assertEqual(g.geometryN(1), QgsPoint(33, 34))
with self.assertRaises(IndexError):
g.removeGeometry(2)

g.fromWkt('GeometryCollection( Point(25 16 37 58), Point(26 22 47 68))')
# get item
with self.assertRaises(IndexError):
g[-3]
with self.assertRaises(IndexError):
g[2]
self.assertEqual(g[0], QgsPoint(25, 16, 37, 58))
self.assertEqual(g[1], QgsPoint(26, 22, 47, 68))
self.assertEqual(g[-2], QgsPoint(25, 16, 37, 58))
self.assertEqual(g[-1], QgsPoint(26, 22, 47, 68))

# del item
g.fromWkt('GeometryCollection( Point(1 2), Point(11 12), Point(33 34))')
with self.assertRaises(IndexError):
del g[-4]
with self.assertRaises(IndexError):
del g[3]
del g[1]
self.assertEqual(len(g), 2)
self.assertEqual(g[0], QgsPoint(1, 2))
self.assertEqual(g[1], QgsPoint(33, 34))
with self.assertRaises(IndexError):
del g[2]

g.fromWkt('GeometryCollection( Point(1 2), Point(11 12), Point(33 34))')
del g[-3]
self.assertEqual(len(g), 2)
self.assertEqual(g[0], QgsPoint(11, 12))
self.assertEqual(g[1], QgsPoint(33, 34))
with self.assertRaises(IndexError):
del g[-3]

def testReferenceGeometry(self):
""" Test parsing a whole range of valid reference wkt formats and variants, and checking
expected values such as length, area, centroids, bounding boxes, etc of the resultant geometry.
Expand Down

0 comments on commit 4bba8ae

Please sign in to comment.