[FEATURE][API] Add some nice PyQGIS methods and exceptions to QgsLineString

- len(QgsCurve) returns number of points in curve
- raise IndexErrors when calling pointN, xAt, yAt, zAt, mAt, setXAt, setYAt,
setMAt, setZAt with invalid vertex indices
- Add [] getter for retrieving specific vertices, eg. ls[0] returns QgsPoint(...)
- Add [] setter for setting specific (existing) vertices, e.g. ls[1] = QgsPoint(1,2)
- Add del support for removing vertices, e.g. del ls[1] removes the second vertex
This commit is contained in:
Nyall Dawson 2018-11-26 11:53:03 +10:00
parent b5ad33f639
commit 1e5479964f
6 changed files with 511 additions and 3 deletions

View File

@ -101,6 +101,20 @@ Returns a list of points within the curve.
Returns the number of points in the curve.
%End
int __len__() const;
%Docstring
Returns the number of points in the curve.
%End
%MethodCode
sipRes = sipCpp->numPoints();
%End
//! Ensures that bool(obj) returns true (otherwise __len__() would be used)
int __bool__() const;
%MethodCode
sipRes = true;
%End
virtual void sumUpArea( double &sum /Out/ ) const = 0;
%Docstring
Sums up the area of the curve by iterating over the vertices (shoelace formula).

View File

@ -81,17 +81,52 @@ Construct a linestring from a single 2d line segment.
virtual bool equals( const QgsCurve &other ) const;
QgsPoint pointN( int i ) const;
SIP_PYOBJECT pointN( int i ) const;
%Docstring
Returns the specified point from inside the line string.
:param i: index of point, starting at 0 for the first point
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
std::unique_ptr< QgsPoint > p = qgis::make_unique< QgsPoint >( sipCpp->pointN( a0 ) );
sipRes = sipConvertFromType( p.release(), sipType_QgsPoint, Py_None );
}
%End
virtual double xAt( int index ) const;
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->xAt( a0 ) );
}
%End
virtual double yAt( int index ) const;
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->yAt( a0 ) );
}
%End
@ -107,6 +142,17 @@ Returns the z-coordinate of the specified node in the line string.
does not have a z dimension
.. seealso:: :py:func:`setZAt`
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->zAt( a0 ) );
}
%End
double mAt( int index ) const;
@ -119,6 +165,17 @@ Returns the m value of the specified node in the line string.
does not have m values
.. seealso:: :py:func:`setMAt`
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->mAt( a0 ) );
}
%End
void setXAt( int index, double x );
@ -130,6 +187,17 @@ Sets the x-coordinate of the specified node in the line string.
:param x: x-coordinate of node
.. seealso:: :py:func:`xAt`
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setXAt( a0, a1 );
}
%End
void setYAt( int index, double y );
@ -141,6 +209,17 @@ Sets the y-coordinate of the specified node in the line string.
:param y: y-coordinate of node
.. seealso:: :py:func:`yAt`
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setYAt( a0, a1 );
}
%End
void setZAt( int index, double z );
@ -152,6 +231,17 @@ Sets the z-coordinate of the specified node in the line string.
:param z: z-coordinate of node
.. seealso:: :py:func:`zAt`
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setZAt( a0, a1 );
}
%End
void setMAt( int index, double m );
@ -163,6 +253,17 @@ Sets the m value of the specified node in the line string.
:param m: m value of node
.. seealso:: :py:func:`mAt`
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setMAt( a0, a1 );
}
%End
void setPoints( const QgsPointSequence &points );
@ -333,6 +434,65 @@ of the curve.
sipRes = PyUnicode_FromString( str.toUtf8().constData() );
%End
SIP_PYOBJECT __getitem__( int index );
%Docstring
Returns the point at the specified ``index``. An IndexError will be raised if no point with the specified ``index`` exists.
.. versionadded:: 3.6
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
std::unique_ptr< QgsPoint > p = qgis::make_unique< QgsPoint >( sipCpp->pointN( a0 ) );
sipRes = sipConvertFromType( p.release(), sipType_QgsPoint, Py_None );
}
%End
void __setitem__( int index, const QgsPoint &point );
%Docstring
Sets the point at the specified ``index``. A point at the ``index`` must already exist or an IndexError will be raised.
.. versionadded:: 3.6
%End
%MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setXAt( a0, a1->x() );
sipCpp->setYAt( a0, a1->y() );
if ( sipCpp->isMeasure() )
sipCpp->setMAt( a0, a1->m() );
if ( sipCpp->is3D() )
sipCpp->setZAt( a0, a1->z() );
}
%End
void __delitem__( int index );
%Docstring
Deletes the vertex at the specified ``index``. A point at the ``index`` must already exist or an IndexError will be raised.
.. versionadded:: 3.6
%End
%MethodCode
if ( a0 >= 0 && a0 < sipCpp->numPoints() )
sipCpp->deleteVertex( QgsVertexId( -1, -1, a0 ) );
else
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
%End
protected:
virtual QgsRectangle calculateBoundingBox() const;

View File

@ -520,14 +520,14 @@ while ($LINE_IDX < $LINE_COUNT){
}
# do not process SIP code %XXXCode
if ( $SIP_RUN == 1 && $LINE =~ m/^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode)(.*)?$/ ){
if ( $SIP_RUN == 1 && $LINE =~ m/^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$/ ){
$LINE = "%$1$2";
$COMMENT = '';
dbg_info("do not process SIP code");
while ( $LINE !~ m/^ *% *End/ ){
write_output("COD", $LINE."\n");
$LINE = read_line();
$LINE =~ s/^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode)(.*)?$/%$1$2/;
$LINE =~ s/^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$/%$1$2/;
$LINE =~ s/^\s*SIP_END(.*)$/%End$1/;
}
$LINE =~ s/^\s*% End/%End/;

View File

@ -106,6 +106,22 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry
*/
virtual int numPoints() const = 0;
#ifdef SIP_RUN
int __len__() const;
% Docstring
Returns the number of points in the curve.
% End
% MethodCode
sipRes = sipCpp->numPoints();
% End
//! Ensures that bool(obj) returns true (otherwise __len__() would be used)
int __bool__() const;
% MethodCode
sipRes = true;
% End
#endif
/**
* Sums up the area of the curve by iterating over the vertices (shoelace formula).
*/

View File

@ -98,10 +98,53 @@ class CORE_EXPORT QgsLineString: public QgsCurve
* Returns the specified point from inside the line string.
* \param i index of point, starting at 0 for the first point
*/
#ifndef SIP_RUN
QgsPoint pointN( int i ) const;
#else
SIP_PYOBJECT pointN( int i ) const;
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
std::unique_ptr< QgsPoint > p = qgis::make_unique< QgsPoint >( sipCpp->pointN( a0 ) );
sipRes = sipConvertFromType( p.release(), sipType_QgsPoint, Py_None );
}
% End
#endif
double xAt( int index ) const override;
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->xAt( a0 ) );
}
% End
#endif
double yAt( int index ) const override;
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->yAt( a0 ) );
}
% End
#endif
/**
* Returns a const pointer to the x vertex data.
@ -171,6 +214,19 @@ class CORE_EXPORT QgsLineString: public QgsCurve
else
return std::numeric_limits<double>::quiet_NaN();
}
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->zAt( a0 ) );
}
% End
#endif
/**
* Returns the m value of the specified node in the line string.
@ -186,6 +242,19 @@ class CORE_EXPORT QgsLineString: public QgsCurve
else
return std::numeric_limits<double>::quiet_NaN();
}
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
return PyFloat_FromDouble( sipCpp->mAt( a0 ) );
}
% End
#endif
/**
* Sets the x-coordinate of the specified node in the line string.
@ -195,6 +264,19 @@ class CORE_EXPORT QgsLineString: public QgsCurve
* \see xAt()
*/
void setXAt( int index, double x );
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setXAt( a0, a1 );
}
% End
#endif
/**
* Sets the y-coordinate of the specified node in the line string.
@ -204,6 +286,19 @@ class CORE_EXPORT QgsLineString: public QgsCurve
* \see yAt()
*/
void setYAt( int index, double y );
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setYAt( a0, a1 );
}
% End
#endif
/**
* Sets the z-coordinate of the specified node in the line string.
@ -217,6 +312,19 @@ class CORE_EXPORT QgsLineString: public QgsCurve
if ( index >= 0 && index < mZ.size() )
mZ[ index ] = z;
}
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setZAt( a0, a1 );
}
% End
#endif
/**
* Sets the m value of the specified node in the line string.
@ -230,6 +338,19 @@ class CORE_EXPORT QgsLineString: public QgsCurve
if ( index >= 0 && index < mM.size() )
mM[ index ] = m;
}
#ifdef SIP_RUN
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setMAt( a0, a1 );
}
% End
#endif
/**
* Resets the line string to match the specified list of points. The line string will
@ -362,6 +483,65 @@ class CORE_EXPORT QgsLineString: public QgsCurve
QString str = QStringLiteral( "<QgsLineString: %1>" ).arg( sipCpp->asWkt() );
sipRes = PyUnicode_FromString( str.toUtf8().constData() );
% End
SIP_PYOBJECT __getitem__( int index );
% Docstring
Returns the point at the specified ``index``. An IndexError will be raised if no point with the specified ``index`` exists.
.. versionadded:: 3.6
% End
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
std::unique_ptr< QgsPoint > p = qgis::make_unique< QgsPoint >( sipCpp->pointN( a0 ) );
sipRes = sipConvertFromType( p.release(), sipType_QgsPoint, Py_None );
}
% End
void __setitem__( int index, const QgsPoint &point );
% Docstring
Sets the point at the specified ``index``. A point at the ``index`` must already exist or an IndexError will be raised.
.. versionadded:: 3.6
% End
% MethodCode
if ( a0 < 0 || a0 >= sipCpp->numPoints() )
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
else
{
sipCpp->setXAt( a0, a1->x() );
sipCpp->setYAt( a0, a1->y() );
if ( sipCpp->isMeasure() )
sipCpp->setMAt( a0, a1->m() );
if ( sipCpp->is3D() )
sipCpp->setZAt( a0, a1->z() );
}
% End
void __delitem__( int index );
% Docstring
Deletes the vertex at the specified ``index``. A point at the ``index`` must already exist or an IndexError will be raised.
.. versionadded:: 3.6
% End
% MethodCode
if ( a0 >= 0 && a0 < sipCpp->numPoints() )
sipCpp->deleteVertex( QgsVertexId( -1, -1, a0 ) );
else
{
PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) );
sipIsErr = 1;
}
% End
#endif
protected:

View File

@ -255,6 +255,144 @@ class TestQgsGeometry(unittest.TestCase):
])
self.assertEqual(myMultiPolygon.wkbType(), QgsWkbTypes.MultiPolygon)
def testLineStringPythonAdditions(self):
"""
Tests Python specific additions to the QgsLineString API
"""
ls = QgsLineString()
self.assertTrue(bool(ls))
self.assertEqual(len(ls), 0)
ls = QgsLineString([QgsPoint(1, 2), QgsPoint(11, 12)])
self.assertTrue(bool(ls))
self.assertEqual(len(ls), 2)
# pointN
with self.assertRaises(IndexError):
ls.pointN(-1)
with self.assertRaises(IndexError):
ls.pointN(2)
self.assertEqual(ls.pointN(0), QgsPoint(1, 2))
self.assertEqual(ls.pointN(1), QgsPoint(11, 12))
# xAt
with self.assertRaises(IndexError):
ls.xAt(-1)
with self.assertRaises(IndexError):
ls.xAt(2)
self.assertEqual(ls.xAt(0), 1)
self.assertEqual(ls.xAt(1), 11)
# yAt
with self.assertRaises(IndexError):
ls.yAt(-1)
with self.assertRaises(IndexError):
ls.yAt(2)
self.assertEqual(ls.yAt(0), 2)
self.assertEqual(ls.yAt(1), 12)
# zAt
with self.assertRaises(IndexError):
ls.zAt(-1)
with self.assertRaises(IndexError):
ls.zAt(2)
# mAt
with self.assertRaises(IndexError):
ls.mAt(-1)
with self.assertRaises(IndexError):
ls.mAt(2)
ls = QgsLineString([QgsPoint(1, 2, 3, 4), QgsPoint(11, 12, 13, 14)])
self.assertEqual(ls.zAt(0), 3)
self.assertEqual(ls.zAt(1), 13)
self.assertEqual(ls.mAt(0), 4)
self.assertEqual(ls.mAt(1), 14)
# setXAt
with self.assertRaises(IndexError):
ls.setXAt(-1, 55)
with self.assertRaises(IndexError):
ls.setXAt(2, 55)
ls.setXAt(0, 5)
ls.setXAt(1, 15)
self.assertEqual(ls.xAt(0), 5)
self.assertEqual(ls.xAt(1), 15)
# setYAt
with self.assertRaises(IndexError):
ls.setYAt(-1, 66)
with self.assertRaises(IndexError):
ls.setYAt(2, 66)
ls.setYAt(0, 6)
ls.setYAt(1, 16)
self.assertEqual(ls.yAt(0), 6)
self.assertEqual(ls.yAt(1), 16)
# setZAt
with self.assertRaises(IndexError):
ls.setZAt(-1, 77)
with self.assertRaises(IndexError):
ls.setZAt(2, 77)
ls.setZAt(0, 7)
ls.setZAt(1, 17)
self.assertEqual(ls.zAt(0), 7)
self.assertEqual(ls.zAt(1), 17)
# setMAt
with self.assertRaises(IndexError):
ls.setMAt(-1, 88)
with self.assertRaises(IndexError):
ls.setMAt(2, 88)
ls.setMAt(0, 8)
ls.setMAt(1, 18)
self.assertEqual(ls.mAt(0), 8)
self.assertEqual(ls.mAt(1), 18)
# get item
with self.assertRaises(IndexError):
ls[-1]
with self.assertRaises(IndexError):
ls[2]
self.assertEqual(ls[0], QgsPoint(5, 6, 7, 8))
self.assertEqual(ls[1], QgsPoint(15, 16, 17, 18))
# set item
with self.assertRaises(IndexError):
ls[-1] = QgsPoint(33, 34)
with self.assertRaises(IndexError):
ls[2] = QgsPoint(33, 34)
ls[0] = QgsPoint(33, 34, 35, 36)
ls[1] = QgsPoint(43, 44, 45, 46)
self.assertEqual(ls[0], QgsPoint(33, 34, 35, 36))
self.assertEqual(ls[1], QgsPoint(43, 44, 45, 46))
# set item, z/m handling
ls[0] = QgsPoint(66, 67)
self.assertEqual(ls[0], QgsPoint(66, 67, None, None, QgsWkbTypes.PointZM))
ls[0] = QgsPoint(77, 78, 79)
self.assertEqual(ls[0], QgsPoint(77, 78, 79, None, QgsWkbTypes.PointZM))
ls[0] = QgsPoint(77, 78, None, 80, QgsWkbTypes.PointZM)
self.assertEqual(ls[0], QgsPoint(77, 78, None, 80, QgsWkbTypes.PointZM))
ls = QgsLineString([QgsPoint(1, 2), QgsPoint(11, 12)])
ls[0] = QgsPoint(66, 67)
self.assertEqual(ls[0], QgsPoint(66, 67))
ls[0] = QgsPoint(86, 87, 89, 90)
self.assertEqual(ls[0], QgsPoint(86, 87))
# del item
ls = QgsLineString([QgsPoint(1, 2), QgsPoint(11, 12), QgsPoint(33, 34)])
with self.assertRaises(IndexError):
del ls[-1]
with self.assertRaises(IndexError):
del ls[3]
del ls[1]
self.assertEqual(len(ls), 2)
self.assertEqual(ls[0], QgsPoint(1, 2))
self.assertEqual(ls[1], QgsPoint(33, 34))
with self.assertRaises(IndexError):
del ls[2]
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.