Skip to content

Commit

Permalink
Add utility function to slice a QPolygonF linestring
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Jul 24, 2020
1 parent 2073790 commit ca5901b
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 1 deletion.
14 changes: 14 additions & 0 deletions python/core/auto_generated/symbology/qgssymbollayerutils.sip.in
Expand Up @@ -721,6 +721,20 @@ Calculate a point on the surface of a QPolygonF
static bool pointInPolygon( const QPolygonF &points, QPointF point );
%Docstring
Calculate whether a point is within of a QPolygonF
%End

static QPolygonF polylineSubstring( const QPolygonF &polyline, double startOffset, double endOffset );
%Docstring
Returns the substring of a ``polyline`` which starts at ``startOffset`` from the beginning of the line
and ends at ``endOffset`` from the start of the line.

If ``startOffset`` is less than 0, then the start point will be calculated by subtracting that distance
from the end of the line. Similarly, if ``endOffset`` is less than zero then the end point will be subtracted
from the end of the line.

May return an empty linestring if the substring is zero length.

.. versionadded:: 3.16
%End

static QgsExpression *fieldOrExpressionToExpression( const QString &fieldOrExpression ) /Factory/;
Expand Down
83 changes: 83 additions & 0 deletions src/core/symbology/qgssymbollayerutils.cpp
Expand Up @@ -4073,6 +4073,89 @@ bool QgsSymbolLayerUtils::pointInPolygon( const QPolygonF &points, QPointF point
return inside;
}

QPolygonF QgsSymbolLayerUtils::polylineSubstring( const QPolygonF &polyline, double startOffset, double endOffset )
{
if ( polyline.size() < 2 )
return QPolygonF();

double totalLength = 0;
auto it = polyline.begin();
QPointF p1 = *it++;
std::vector< double > segmentLengths( polyline.size() - 1 );
auto segmentLengthIt = segmentLengths.begin();
for ( ; it != polyline.end(); ++it )
{
QPointF p2 = *it;
*segmentLengthIt = std::sqrt( std::pow( p1.x() - p2.x(), 2.0 ) + std::pow( p1.y() - p2.y(), 2.0 ) );
totalLength += *segmentLengthIt;

segmentLengthIt++;
p1 = p2;
}

if ( startOffset >= 0 && totalLength <= startOffset )
return QPolygonF();
if ( endOffset < 0 && totalLength <= -endOffset )
return QPolygonF();

const double startDistance = startOffset < 0 ? totalLength + startOffset : startOffset;
const double endDistance = endOffset <= 0 ? totalLength + endOffset : endOffset;
QPolygonF substringPoints;
substringPoints.reserve( polyline.size() );

it = polyline.begin();
segmentLengthIt = segmentLengths.begin();

p1 = *it++;
bool foundStart = false;
if ( qgsDoubleNear( startDistance, 0.0 ) || startDistance < 0 )
{
substringPoints << p1;
foundStart = true;
}

double distanceTraversed = 0;
for ( ; it != polyline.end(); ++it )
{
QPointF p2 = *it;
if ( distanceTraversed < startDistance && distanceTraversed + *segmentLengthIt > startDistance )
{
// start point falls on this segment
const double distanceToStart = startDistance - distanceTraversed;
double startX, startY;
QgsGeometryUtils::pointOnLineWithDistance( p1.x(), p1.y(), p2.x(), p2.y(), distanceToStart, startX, startY );
substringPoints << QPointF( startX, startY );
foundStart = true;
}
if ( foundStart && ( distanceTraversed + *segmentLengthIt > endDistance ) )
{
// end point falls on this segment
const double distanceToEnd = endDistance - distanceTraversed;
double endX, endY;
QgsGeometryUtils::pointOnLineWithDistance( p1.x(), p1.y(), p2.x(), p2.y(), distanceToEnd, endX, endY );
if ( substringPoints.last() != QPointF( endX, endY ) )
substringPoints << QPointF( endX, endY );
}
else if ( foundStart )
{
if ( substringPoints.last() != QPointF( p2.x(), p2.y() ) )
substringPoints << QPointF( p2.x(), p2.y() );
}

distanceTraversed += *segmentLengthIt;
if ( distanceTraversed > endDistance )
break;

p1 = p2;
segmentLengthIt++;
}

if ( ( substringPoints.size() < 2 ) || ( substringPoints.size() == 2 && substringPoints.at( 0 ) == substringPoints.at( 1 ) ) )
return QPolygonF();

return substringPoints;
}

QgsExpression *QgsSymbolLayerUtils::fieldOrExpressionToExpression( const QString &fieldOrExpression )
{
if ( fieldOrExpression.isEmpty() )
Expand Down
14 changes: 14 additions & 0 deletions src/core/symbology/qgssymbollayerutils.h
Expand Up @@ -648,6 +648,20 @@ class CORE_EXPORT QgsSymbolLayerUtils
//! Calculate whether a point is within of a QPolygonF
static bool pointInPolygon( const QPolygonF &points, QPointF point );

/**
* Returns the substring of a \a polyline which starts at \a startOffset from the beginning of the line
* and ends at \a endOffset from the start of the line.
*
* If \a startOffset is less than 0, then the start point will be calculated by subtracting that distance
* from the end of the line. Similarly, if \a endOffset is less than zero then the end point will be subtracted
* from the end of the line.
*
* May return an empty linestring if the substring is zero length.
*
* \since QGIS 3.16
*/
static QPolygonF polylineSubstring( const QPolygonF &polyline, double startOffset, double endOffset );

/**
* Returns a new valid expression instance for given field or expression string.
* If the input is not a valid expression, it is assumed that it is a field name and gets properly quoted.
Expand Down
54 changes: 53 additions & 1 deletion tests/src/python/test_qgssymbollayerutils.py
Expand Up @@ -16,7 +16,7 @@
QgsMarkerSymbol,
QgsArrowSymbolLayer,
QgsUnitTypes)
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtGui import QColor, QPolygonF
from qgis.PyQt.QtCore import QSizeF, QPointF
from qgis.testing import unittest, start_app

Expand Down Expand Up @@ -246,6 +246,58 @@ def testDecodeSldUom(self):
decode = QgsSymbolLayerUtils.decodeSldUom("http://www.opengeospatial.org/se/units/pixel")
self.assertEqual(decode, (QgsUnitTypes.RenderPixels, 1.0))

def testPolylineSubstring(self):
res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF(), 1, 2) # no crash
self.assertFalse(res)

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF(), -1, 2) # no crash
self.assertFalse(res)

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF(), 1, -2) # no crash
self.assertFalse(res)

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF(), -1, -2) # no crash
self.assertFalse(res)

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 0, -110)
self.assertEqual([p for p in res], [])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 0, 110)
self.assertEqual([p for p in res], [QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), -1, -1000)
self.assertFalse([p for p in res])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 1, -1000)
self.assertFalse([p for p in res])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), -1, 1000)
self.assertEqual([p for p in res], [QPointF(110.0, 12.0), QPointF(111.0, 12.0)])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 100000, -10000)
self.assertFalse([p for p in res])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 1, -109)
self.assertEqual([p for p in res], [])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 1, 109)
self.assertEqual([p for p in res], [QPointF(11.0, 3.0), QPointF(11.0, 12.0), QPointF(110.0, 12.0)])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), -109, 109)
self.assertEqual([p for p in res], [QPointF(11.0, 3.0), QPointF(11.0, 12.0), QPointF(110.0, 12.0)])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 1, -1000)
self.assertEqual([p for p in res], [])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 1, 10)
self.assertEqual([p for p in res], [QPointF(11, 3), QPointF(11, 12)])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 1, 0)
self.assertEqual([p for p in res], [QPointF(11, 3), QPointF(11, 12), QPointF(111, 12)])

res = QgsSymbolLayerUtils.polylineSubstring(QPolygonF([QPointF(11, 2), QPointF(11, 12), QPointF(111, 12)]), 1, -90)
self.assertEqual([p for p in res], [QPointF(11, 3), QPointF(11, 12), QPointF(21, 12)])


if __name__ == '__main__':
unittest.main()

0 comments on commit ca5901b

Please sign in to comment.