Skip to content
Permalink
Browse files
Add method to "scroll" the vertices of a closed curve geometry,
so that a particular vertex is the first one
  • Loading branch information
nyalldawson committed Apr 28, 2021
1 parent 2a48e2b commit e3a73a1ac5e7dd5d1df3f13627aa40a21200e62c
@@ -222,6 +222,7 @@ Appends the contents of another circular ``string`` to the end of this circular
int compareToSameClass( const QgsAbstractGeometry *other ) const final;
virtual QgsRectangle calculateBoundingBox() const;

void scroll( int firstVertexIndex ) final;

};

@@ -206,6 +206,7 @@ Appends first point if not already closed.
int compareToSameClass( const QgsAbstractGeometry *other ) const final;
virtual QgsRectangle calculateBoundingBox() const;

void scroll( int firstVertexIndex ) final;

};

@@ -296,6 +296,24 @@ Returns the curve's orientation, e.g. clockwise or counter-clockwise.
virtual QgsPoint childPoint( int index ) const;


virtual void scroll( int firstVertexIndex ) = 0;
%Docstring
Scrolls the curve vertices so that they start with the vertex at the given index.

.. warning::

This should only be called on closed curves, or the shape of the curve will be altered and
the result is undefined.

.. warning::

The ``firstVertexIndex`` must correspond to a segment vertex and not a curve point or the result
is undefined.


.. versionadded:: 3.20
%End



};
@@ -652,6 +652,7 @@ corresponds to the last point in the line.
int compareToSameClass( const QgsAbstractGeometry *other ) const final;
virtual QgsRectangle calculateBoundingBox() const;

void scroll( int firstVertexIndex ) final;

};

@@ -297,6 +297,44 @@ QgsRectangle QgsCircularString::calculateBoundingBox() const
return bbox;
}

void QgsCircularString::scroll( int index )
{
const int size = mX.size();
if ( index < 1 || index >= size - 1 )
return;

const bool useZ = is3D();
const bool useM = isMeasure();

QVector<double> newX( size );
QVector<double> newY( size );
QVector<double> newZ( useZ ? size : 0 );
QVector<double> newM( useM ? size : 0 );
auto it = std::copy( mX.constBegin() + index, mX.constEnd() - 1, newX.begin() );
it = std::copy( mX.constBegin(), mX.constBegin() + index, it );
*it = *newX.constBegin();
mX = std::move( newX );

it = std::copy( mY.constBegin() + index, mY.constEnd() - 1, newY.begin() );
it = std::copy( mY.constBegin(), mY.constBegin() + index, it );
*it = *newY.constBegin();
mY = std::move( newY );
if ( useZ )
{
it = std::copy( mZ.constBegin() + index, mZ.constEnd() - 1, newZ.begin() );
it = std::copy( mZ.constBegin(), mZ.constBegin() + index, it );
*it = *newZ.constBegin();
mZ = std::move( newZ );
}
if ( useM )
{
it = std::copy( mM.constBegin() + index, mM.constEnd() - 1, newM.begin() );
it = std::copy( mM.constBegin(), mM.constBegin() + index, it );
*it = *newM.constBegin();
mM = std::move( newM );
}
}

QgsRectangle QgsCircularString::segmentBoundingBox( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3 )
{
double centerX, centerY, radius;
@@ -201,6 +201,7 @@ class CORE_EXPORT QgsCircularString: public QgsCurve

int compareToSameClass( const QgsAbstractGeometry *other ) const final;
QgsRectangle calculateBoundingBox() const override;
void scroll( int firstVertexIndex ) final;

private:
QVector<double> mX;
@@ -161,6 +161,28 @@ QgsRectangle QgsCompoundCurve::calculateBoundingBox() const
return bbox;
}

void QgsCompoundCurve::scroll( int index )
{
const int size = numPoints();
if ( index < 1 || index >= size - 1 )
return;

auto [p1, p2 ] = splitCurveAtVertex( index );

mCurves.clear();
if ( QgsCompoundCurve *curve2 = qgsgeometry_cast< QgsCompoundCurve *>( p2.get() ) )
{
// take the curves from the second part and make them our first lot of curves
mCurves = std::move( curve2->mCurves );
}
if ( QgsCompoundCurve *curve1 = qgsgeometry_cast< QgsCompoundCurve *>( p1.get() ) )
{
// take the curves from the first part and append them to our curves
mCurves.append( curve1->mCurves );
curve1->mCurves.clear();
}
}

bool QgsCompoundCurve::fromWkb( QgsConstWkbPtr &wkbPtr )
{
clear();
@@ -186,6 +186,7 @@ class CORE_EXPORT QgsCompoundCurve: public QgsCurve

int compareToSameClass( const QgsAbstractGeometry *other ) const final;
QgsRectangle calculateBoundingBox() const override;
void scroll( int firstVertexIndex ) final;

private:
QVector< QgsCurve * > mCurves;
@@ -306,6 +306,19 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry SIP_ABSTRACT
int childCount() const override;
QgsPoint childPoint( int index ) const override;

/**
* Scrolls the curve vertices so that they start with the vertex at the given index.
*
* \warning This should only be called on closed curves, or the shape of the curve will be altered and
* the result is undefined.
*
* \warning The \a firstVertexIndex must correspond to a segment vertex and not a curve point or the result
* is undefined.
*
* \since QGIS 3.20
*/
virtual void scroll( int firstVertexIndex ) = 0;

#ifndef SIP_RUN

/**
@@ -326,6 +339,8 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry SIP_ABSTRACT

mutable bool mHasCachedValidity = false;
mutable QString mValidityFailureReason;

friend class TestQgsGeometry;
};

#endif // QGSCURVE_H
@@ -581,6 +581,44 @@ QgsRectangle QgsLineString::calculateBoundingBox() const
return QgsRectangle( xmin, ymin, xmax, ymax, false );
}

void QgsLineString::scroll( int index )
{
const int size = mX.size();
if ( index < 1 || index >= size - 1 )
return;

const bool useZ = is3D();
const bool useM = isMeasure();

QVector<double> newX( size );
QVector<double> newY( size );
QVector<double> newZ( useZ ? size : 0 );
QVector<double> newM( useM ? size : 0 );
auto it = std::copy( mX.constBegin() + index, mX.constEnd() - 1, newX.begin() );
it = std::copy( mX.constBegin(), mX.constBegin() + index, it );
*it = *newX.constBegin();
mX = std::move( newX );

it = std::copy( mY.constBegin() + index, mY.constEnd() - 1, newY.begin() );
it = std::copy( mY.constBegin(), mY.constBegin() + index, it );
*it = *newY.constBegin();
mY = std::move( newY );
if ( useZ )
{
it = std::copy( mZ.constBegin() + index, mZ.constEnd() - 1, newZ.begin() );
it = std::copy( mZ.constBegin(), mZ.constBegin() + index, it );
*it = *newZ.constBegin();
mZ = std::move( newZ );
}
if ( useM )
{
it = std::copy( mM.constBegin() + index, mM.constEnd() - 1, newM.begin() );
it = std::copy( mM.constBegin(), mM.constBegin() + index, it );
*it = *newM.constBegin();
mM = std::move( newM );
}
}

/***************************************************************************
* This class is considered CRITICAL and any change MUST be accompanied with
* full unit tests.
@@ -798,6 +798,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve

int compareToSameClass( const QgsAbstractGeometry *other ) const final;
QgsRectangle calculateBoundingBox() const override;
void scroll( int firstVertexIndex ) final;

private:
QVector<double> mX;
@@ -820,6 +821,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve

friend class QgsPolygon;
friend class QgsTriangle;
friend class TestQgsGeometry;

};

@@ -166,6 +166,9 @@ class TestQgsGeometry : public QObject
void compareTo_data();
void compareTo();

void scroll_data();
void scroll();

// MK, Disabled 14.11.2014
// Too unclear what exactly should be tested and which variations are allowed for the line
#if 0
QCOMPARE( QgsGeometry::fromWkt( geom1 ).get()->compareTo( QgsGeometry::fromWkt( geom2 ).get() ), expected );
}

void TestQgsGeometry::scroll_data()
{
QTest::addColumn<QString>( "curve" );
QTest::addColumn<int>( "vertex" );
QTest::addColumn<QString>( "expected" );

QTest::newRow( "linestring empty" ) << QStringLiteral( "LINESTRING()" ) << 2 << QStringLiteral( "LineString EMPTY" );
QTest::newRow( "linestring no matching point" ) << QStringLiteral( "LINESTRING( 1 1, 1 2, 1 3)" ) << 4 << QStringLiteral( "LineString (1 1, 1 2, 1 3)" );
QTest::newRow( "linestring one vertex" ) << QStringLiteral( "LINESTRING( 1 1)" ) << 0 << QStringLiteral( "LineString (1 1)" );
QTest::newRow( "linestring match first" ) << QStringLiteral( "LINESTRING( 1 1, 1 2, 1 3, 1 4, 1 1)" ) << 0 << QStringLiteral( "LineString (1 1, 1 2, 1 3, 1 4, 1 1)" );
QTest::newRow( "linestring match second" ) << QStringLiteral( "LINESTRING( 1 1, 1 2, 1 3, 1 4, 1 1)" ) << 1 << QStringLiteral( "LineString (1 2, 1 3, 1 4, 1 1, 1 2)" );
QTest::newRow( "linestring match third" ) << QStringLiteral( "LINESTRING( 1 1, 1 2, 1 3, 1 4, 1 1)" ) << 2 << QStringLiteral( "LineString (1 3, 1 4, 1 1, 1 2, 1 3)" );
QTest::newRow( "linestring match forth" ) << QStringLiteral( "LINESTRING( 1 1, 1 2, 1 3, 1 4, 1 1)" ) << 3 << QStringLiteral( "LineString (1 4, 1 1, 1 2, 1 3, 1 4)" );
QTest::newRow( "linestring match last" ) << QStringLiteral( "LINESTRING( 1 1, 1 2, 1 3, 1 4, 1 1)" ) << 4 << QStringLiteral( "LineString (1 1, 1 2, 1 3, 1 4, 1 1)" );
QTest::newRow( "linestringz" ) << QStringLiteral( "LINESTRINGZ( 1 1 3 , 1 2 4, 1 3 5 , 1 4 6, 1 1 3)" ) << 2 << QStringLiteral( "LineStringZ (1 3 5, 1 4 6, 1 1 3, 1 2 4, 1 3 5)" );
QTest::newRow( "linestringm" ) << QStringLiteral( "LINESTRINGM( 1 1 3 , 1 2 4, 1 3 5 , 1 4 6, 1 1 3)" ) << 2 << QStringLiteral( "LineStringM (1 3 5, 1 4 6, 1 1 3, 1 2 4, 1 3 5)" );
QTest::newRow( "linestringzm" ) << QStringLiteral( "LINESTRINGZM( 1 1 3 5, 1 2 4 6, 1 3 5 7, 1 4 6 8, 1 1 3 5)" ) << 2 << QStringLiteral( "LineStringZM (1 3 5 7, 1 4 6 8, 1 1 3 5, 1 2 4 6, 1 3 5 7)" );

QTest::newRow( "circularstring empty" ) << QStringLiteral( "CIRCULARSTRING()" ) << 2 << QStringLiteral( "CircularString EMPTY" );
QTest::newRow( "circularstring no matching point" ) << QStringLiteral( "CIRCULARSTRING( 1 1, 1 2, 1 3)" ) << 4 << QStringLiteral( "CircularString (1 1, 1 2, 1 3)" );
// technically not valid, but we don't want a crash
QTest::newRow( "circularstring one vertex" ) << QStringLiteral( "CIRCULARSTRING( 1 1)" ) << 0 << QStringLiteral( "CircularString (1 1)" );
QTest::newRow( "circularstring match first" ) << QStringLiteral( "CIRCULARSTRING( 1 1, 2 1, 2 2, 1 4, 1 1)" ) << 0 << QStringLiteral( "CircularString (1 1, 2 1, 2 2, 1 4, 1 1)" );
QTest::newRow( "circularstring match third" ) << QStringLiteral( "CIRCULARSTRING( 1 1, 2 1, 2 2, 1 4, 1 1)" ) << 2 << QStringLiteral( "CircularString (2 2, 1 4, 1 1, 2 1, 2 2)" );
QTest::newRow( "circularstring match 5th" ) << QStringLiteral( "CIRCULARSTRING( 1 1, 2 1, 2 2, 3 4, 2 4, 1 4, 1 1)" ) << 4 << QStringLiteral( "CircularString (2 4, 1 4, 1 1, 2 1, 2 2, 3 4, 2 4)" );
QTest::newRow( "circularstring match last" ) << QStringLiteral( "CIRCULARSTRING( 1 1, 2 1, 2 2, 3 4, 2 4, 1 4, 1 1)" ) << 6 << QStringLiteral( "CircularString (1 1, 2 1, 2 2, 3 4, 2 4, 1 4, 1 1)" );
QTest::newRow( "circularstringz" ) << QStringLiteral( "CIRCULARSTRINGZ( 1 1 3 , 2 1 4, 2 2 5 , 1 4 6, 1 1 3)" ) << 2 << QStringLiteral( "CircularStringZ (2 2 5, 1 4 6, 1 1 3, 2 1 4, 2 2 5)" );
QTest::newRow( "circularstringm" ) << QStringLiteral( "CIRCULARSTRINGM( 1 1 3 , 2 1 4, 2 2 5 , 1 4 6, 1 1 3)" ) << 2 << QStringLiteral( "CircularStringM (2 2 5, 1 4 6, 1 1 3, 2 1 4, 2 2 5)" );
QTest::newRow( "circularstringzm" ) << QStringLiteral( "CIRCULARSTRINGZM( 1 1 3 5, 2 1 4 6, 2 2 5 7, 1 4 6 8, 1 1 3 5)" ) << 2 << QStringLiteral( "CircularStringZM (2 2 5 7, 1 4 6 8, 1 1 3 5, 2 1 4 6, 2 2 5 7)" );

QTest::newRow( "compoundcurve empty" ) << QStringLiteral( "COMPOUNDCURVE()" ) << 2 << QStringLiteral( "CompoundCurve EMPTY" );
QTest::newRow( "compoundcurve no matching point" ) << QStringLiteral( "COMPOUNDCURVE(( 1 1, 1 2, 1 3))" ) << 4 << QStringLiteral( "CompoundCurve ((1 1, 1 2, 1 3))" );
QTest::newRow( "compoundcurve one vertex" ) << QStringLiteral( "COMPOUNDCURVE(( 1 1))" ) << 0 << QStringLiteral( "CompoundCurve ((1 1))" );
QTest::newRow( "compoundcurve match first" ) << QStringLiteral( "COMPOUNDCURVE(( 1 1, 1 2),( 1 2, 1 3, 1 4, 1 1))" ) << 0 << QStringLiteral( "CompoundCurve ((1 1, 1 2),(1 2, 1 3, 1 4, 1 1))" );
QTest::newRow( "compoundcurve match second" ) << QStringLiteral( "COMPOUNDCURVE(( 1 1, 1 2),( 1 2, 1 3, 1 4, 1 1))" ) << 1 << QStringLiteral( "CompoundCurve ((1 2, 1 3, 1 4, 1 1),(1 1, 1 2))" );
QTest::newRow( "compoundcurve match third" ) << QStringLiteral( "COMPOUNDCURVE(( 1 1, 1 2),( 1 2, 1 3, 1 4, 1 1))" ) << 2 << QStringLiteral( "CompoundCurve ((1 3, 1 4, 1 1),(1 1, 1 2),(1 2, 1 3))" );
QTest::newRow( "compoundcurve match forth" ) << QStringLiteral( "COMPOUNDCURVE(( 1 1, 1 2),( 1 2, 1 3, 1 4, 1 1))" ) << 3 << QStringLiteral( "CompoundCurve ((1 4, 1 1),(1 1, 1 2),(1 2, 1 3, 1 4))" );
QTest::newRow( "compoundcurve match last" ) << QStringLiteral( "COMPOUNDCURVE(( 1 1, 1 2),( 1 2, 1 3, 1 4, 1 1))" ) << 4 << QStringLiteral( "CompoundCurve ((1 1, 1 2),(1 2, 1 3, 1 4, 1 1))" );
QTest::newRow( "compoundcurvez" ) << QStringLiteral( "COMPOUNDCURVEZ(( 1 1 3 , 1 2 4, 1 3 5 , 1 4 6, 1 1 3))" ) << 2 << QStringLiteral( "CompoundCurveZ ((1 3 5, 1 4 6, 1 1 3),(1 1 3, 1 2 4, 1 3 5))" );
QTest::newRow( "compoundcurvem" ) << QStringLiteral( "COMPOUNDCURVEM(( 1 1 3 , 1 2 4, 1 3 5 , 1 4 6, 1 1 3))" ) << 2 << QStringLiteral( "CompoundCurveM ((1 3 5, 1 4 6, 1 1 3),(1 1 3, 1 2 4, 1 3 5))" );
QTest::newRow( "compoundcurvezm" ) << QStringLiteral( "COMPOUNDCURVEZM(( 1 1 3 5, 1 2 4 6, 1 3 5 7, 1 4 6 8, 1 1 3 5))" ) << 2 << QStringLiteral( "CompoundCurveZM ((1 3 5 7, 1 4 6 8, 1 1 3 5),(1 1 3 5, 1 2 4 6, 1 3 5 7))" );
}

void TestQgsGeometry::scroll()
{
QFETCH( QString, curve );
QFETCH( int, vertex );
QFETCH( QString, expected );

QgsGeometry geom = QgsGeometry::fromWkt( curve );
qgsgeometry_cast< QgsCurve * >( geom.get() )->scroll( vertex );

QCOMPARE( geom.get()->asWkt(), expected );
}

// MK, Disabled 14.11.2014
// Too unclear what exactly should be tested and which variations are allowed for the line
#if 0

0 comments on commit e3a73a1

Please sign in to comment.