Consider curved geometries in marker line symbol layer

This commit is contained in:
Marco Hugentobler 2015-09-04 14:53:06 +02:00
parent a5a963ccfe
commit 56316ddcad
24 changed files with 386 additions and 3 deletions

View File

@ -116,4 +116,8 @@ class QgsAbstractGeometryV2
virtual bool hasCurvedSegments() const;
/** Returns a geometry without curves. Caller takes ownership*/
virtual QgsAbstractGeometryV2* segmentize() const /Factory/;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
virtual double vertexAngle( const QgsVertexId& vertex ) const = 0;
};

View File

@ -55,6 +55,10 @@ class QgsCircularStringV2: public QgsCurveV2
bool hasCurvedSegments() const;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const;
private:
void segmentize( const QgsPointV2& p1, const QgsPointV2& p2, const QgsPointV2& p3, QList<QgsPointV2>& points ) const;
};

View File

@ -60,4 +60,8 @@ class QgsCompoundCurveV2: public QgsCurveV2
void sumUpArea( double& sum ) const;
bool hasCurvedSegments() const;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const;
};

View File

@ -62,4 +62,8 @@ class QgsCurvePolygonV2: public QgsSurfaceV2
bool hasCurvedSegments() const;
QgsAbstractGeometryV2* segmentize() const /Factory/;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const;
};

View File

@ -51,4 +51,8 @@ class QgsGeometryCollectionV2: public QgsAbstractGeometryV2
virtual double area() const;
bool hasCurvedSegments() const;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const;
};

View File

@ -55,4 +55,8 @@ class QgsLineStringV2: public QgsCurveV2
bool pointAt( int i, QgsPointV2& vertex, QgsVertexId::VertexType& type ) const;
void sumUpArea( double& sum ) const;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const;
};

View File

@ -55,4 +55,7 @@ class QgsPointV2: public QgsAbstractGeometryV2
double closestSegment( const QgsPointV2& pt, QgsPointV2& segmentPt, QgsVertexId& vertexAfter, bool* leftOf, double epsilon ) const;
bool nextVertex( QgsVertexId& id, QgsPointV2& vertex ) const;
/** Angle undefined. Always returns 0.0*/
double vertexAngle( const QgsVertexId& vertex ) const;
};

View File

@ -317,6 +317,10 @@ class CORE_EXPORT QgsAbstractGeometryV2
*/
virtual QgsAbstractGeometryV2* segmentize() const { return clone(); }
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
virtual double vertexAngle( const QgsVertexId& vertex ) const = 0;
protected:
QgsWKBTypes::Type mWkbType;
mutable QgsRectangle mBoundingBox;

View File

@ -953,3 +953,58 @@ void QgsCircularStringV2::insertVertexBetween( int after, int before, int pointO
mM.insert( before, ( mM[after] + mM[before] ) / 2.0 );
}
}
double QgsCircularStringV2::vertexAngle( const QgsVertexId& vId ) const
{
int before = vId.vertex - 1;
int vertex = vId.vertex;
int after = vId.vertex + 1;
if ( vId.vertex % 2 != 0 ) // a curve vertex
{
if ( vId.vertex >= 1 && vId.vertex < numPoints() - 1 )
{
return QgsGeometryUtils::circleTangentDirection( QgsPointV2( mX[vertex], mY[vertex] ), QgsPointV2( mX[before], mY[before] ),
QgsPointV2( mX[vertex], mY[vertex] ), QgsPointV2( mX[after], mY[after] ) );
}
}
else //a point vertex
{
if ( vId.vertex == 0 )
{
return QgsGeometryUtils::circleTangentDirection( QgsPointV2( mX[0], mY[0] ), QgsPointV2( mX[0], mY[0] ),
QgsPointV2( mX[1], mY[1] ), QgsPointV2( mX[2], mY[2] ) );
}
if ( vId.vertex >= numPoints() - 1 )
{
if ( numPoints() < 3 )
{
return 0.0;
}
int a = numPoints() - 3;
int b = numPoints() - 2;
int c = numPoints() - 1;
return QgsGeometryUtils::circleTangentDirection( QgsPointV2( mX[c], mY[c] ), QgsPointV2( mX[a], mY[a] ),
QgsPointV2( mX[b], mY[b] ), QgsPointV2( mX[c], mY[c] ) );
}
else
{
if ( vId.vertex + 2 > numPoints() - 1 )
{
return 0.0;
}
int vertex1 = vId.vertex - 2;
int vertex2 = vId.vertex - 1;
int vertex3 = vId.vertex;
double angle1 = QgsGeometryUtils::circleTangentDirection( QgsPointV2( mX[vertex3], mY[vertex3] ),
QgsPointV2( mX[vertex1], mY[vertex1] ), QgsPointV2( mX[vertex2], mY[vertex2] ), QgsPointV2( mX[vertex3], mY[vertex3] ) );
int vertex4 = vId.vertex + 1;
int vertex5 = vId.vertex + 2;
double angle2 = QgsGeometryUtils::circleTangentDirection( QgsPointV2( mX[vertex3], mY[vertex3] ),
QgsPointV2( mX[vertex3], mY[vertex3] ), QgsPointV2( mX[vertex4], mY[vertex4] ), QgsPointV2( mX[vertex5], mY[vertex5] ) );
return QgsGeometryUtils::averageAngle( angle1, angle2 );
}
}
return 0.0;
}

View File

@ -91,6 +91,10 @@ class CORE_EXPORT QgsCircularStringV2: public QgsCurveV2
bool hasCurvedSegments() const override { return true; }
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const override;
private:
QVector<double> mX;
QVector<double> mY;

View File

@ -586,3 +586,25 @@ bool QgsCompoundCurveV2::hasCurvedSegments() const
return false;
}
double QgsCompoundCurveV2::vertexAngle( const QgsVertexId& vertex ) const
{
QList< QPair<int, QgsVertexId> > curveIds = curveVertexId( vertex );
if ( curveIds.size() == 1 )
{
QgsCurveV2* curve = mCurves[curveIds.at( 0 ).first];
return curve->vertexAngle( curveIds.at( 0 ).second );
}
else if ( curveIds.size() > 1 )
{
QgsCurveV2* curve1 = mCurves[curveIds.at( 0 ).first];
QgsCurveV2* curve2 = mCurves[curveIds.at( 1 ).first];
double angle1 = curve1->vertexAngle( curveIds.at( 0 ).second );
double angle2 = curve2->vertexAngle( curveIds.at( 1 ).second );
return QgsGeometryUtils::averageAngle( angle1, angle2 );
}
else
{
return 0.0;
}
}

View File

@ -104,6 +104,10 @@ class CORE_EXPORT QgsCompoundCurveV2: public QgsCurveV2
bool hasCurvedSegments() const override;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const override;
private:
QList< QgsCurveV2* > mCurves;
/** Turns a vertex id for the compound curve into one or more ids for the subcurves

View File

@ -665,3 +665,14 @@ QgsAbstractGeometryV2* QgsCurvePolygonV2::segmentize() const
{
return toPolygon();
}
double QgsCurvePolygonV2::vertexAngle( const QgsVertexId& vertex ) const
{
if ( !mExteriorRing || vertex.ring < 0 || vertex.ring >= 1 + mInteriorRings.size() )
{
return false;
}
QgsCurveV2* ring = vertex.ring == 0 ? mExteriorRing : mInteriorRings[vertex.ring - 1];
return ring->vertexAngle( vertex );
}

View File

@ -94,6 +94,10 @@ class CORE_EXPORT QgsCurvePolygonV2: public QgsSurfaceV2
bool hasCurvedSegments() const override;
QgsAbstractGeometryV2* segmentize() const override;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const override;
protected:
QgsCurveV2* mExteriorRing;

View File

@ -509,3 +509,19 @@ QgsAbstractGeometryV2* QgsGeometryCollectionV2::segmentize() const
}
return geomCollection;
}
double QgsGeometryCollectionV2::vertexAngle( const QgsVertexId& vertex ) const
{
if ( vertex.part >= mGeometries.size() )
{
return 0.0;
}
QgsAbstractGeometryV2* geom = mGeometries[vertex.part];
if ( !geom )
{
return 0.0;
}
return geom->vertexAngle( vertex );
}

View File

@ -102,6 +102,10 @@ class CORE_EXPORT QgsGeometryCollectionV2: public QgsAbstractGeometryV2
/** Returns a geometry without curves. Caller takes ownership*/
QgsAbstractGeometryV2* segmentize() const override;
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const override;
protected:
QVector< QgsAbstractGeometryV2* > mGeometries;

View File

@ -357,6 +357,26 @@ bool QgsGeometryUtils::segmentMidPoint( const QgsPointV2& p1, const QgsPointV2&
return true;
}
double QgsGeometryUtils::circleTangentDirection( const QgsPointV2& tangentPoint, const QgsPointV2& cp1,
const QgsPointV2& cp2, const QgsPointV2& cp3 )
{
//calculate circle midpoint
double mX, mY, radius;
circleCenterRadius( cp1, cp2, cp3, radius, mX, mY );
double p1Angle = QgsGeometryUtils::ccwAngle( cp1.y() - mY, cp1.x() - mX );
double p2Angle = QgsGeometryUtils::ccwAngle( cp2.y() - mY, cp2.x() - mX );
double p3Angle = QgsGeometryUtils::ccwAngle( cp3.y() - mY, cp3.x() - mX );
if ( circleClockwise( p1Angle, p2Angle, p3Angle ) )
{
return lineAngle( tangentPoint.x(), tangentPoint.y(), mX, mY );
}
else
{
return lineAngle( mX, mY, tangentPoint.x(), tangentPoint.y() );
}
}
QList<QgsPointV2> QgsGeometryUtils::pointsFromWKT( const QString &wktCoordinateList, bool is3D, bool isMeasure )
{
int dim = 2 + is3D + isMeasure;
@ -525,3 +545,71 @@ QStringList QgsGeometryUtils::wktGetChildBlocks( const QString &wkt, const QStri
}
return blocks;
}
double QgsGeometryUtils::lineAngle( double x1, double y1, double x2, double y2 )
{
double at = atan2( y2 - y1, x2 - x1 );
double a = -at + M_PI / 2.0;
if ( a < 0 )
{
a = 2 * M_PI + a;
}
if ( a >= 2 * M_PI )
{
a -= 2 * M_PI;
}
return a;
}
double QgsGeometryUtils::linePerpendicularAngle( double x1, double y1, double x2, double y2 )
{
double a = lineAngle( x1, y1, x2, y2 );
a += ( M_PI / 2.0 );
if ( a >= 2 * M_PI )
{
a -= ( 2 * M_PI );
}
return a;
}
double QgsGeometryUtils::averageAngle( double x1, double y1, double x2, double y2, double x3, double y3 )
{
// calc average angle between the previous and next point
double a1 = linePerpendicularAngle( x1, y1, x2, y2 );
double a2 = linePerpendicularAngle( x2, y2, x3, y3 );
return averageAngle( a1, a2 );
}
double QgsGeometryUtils::averageAngle( double a1, double a2 )
{
double clockwiseDiff = 0.0;
if ( a2 >= a1 )
{
clockwiseDiff = a2 - a1;
}
else
{
clockwiseDiff = a2 + ( 2 * M_PI - a1 );
}
double counterClockwiseDiff = 2 * M_PI - clockwiseDiff;
double resultAngle = 0;
if ( clockwiseDiff <= counterClockwiseDiff )
{
resultAngle = a1 + clockwiseDiff / 2.0;
}
else
{
resultAngle = a1 - counterClockwiseDiff / 2.0;
}
if ( resultAngle >= 2 * M_PI )
{
resultAngle -= 2 * M_PI;
}
else if ( resultAngle < 0 )
{
resultAngle = 2 * M_PI - resultAngle;
}
return resultAngle;
}

View File

@ -79,6 +79,9 @@ class CORE_EXPORT QgsGeometryUtils
/** Calculates midpoint on circle passing through p1 and p2, closest to given coordinate*/
static bool segmentMidPoint( const QgsPointV2& p1, const QgsPointV2& p2, QgsPointV2& result, double radius, const QgsPointV2& mousePos );
/** Calculates the direction angle of a circle tangent (clockwise from north in radians)*/
static double circleTangentDirection( const QgsPointV2& tangentPoint, const QgsPointV2& cp1, const QgsPointV2& cp2, const QgsPointV2& cp3 );
/** Returns a list of points contained in a WKT string.
*/
static QList<QgsPointV2> pointsFromWKT( const QString& wktCoordinateList, bool is3D, bool isMeasure );
@ -93,6 +96,12 @@ class CORE_EXPORT QgsGeometryUtils
/** Returns a geoJSON coordinates string */
static QString pointsToJSON( const QList<QgsPointV2>& points, int precision );
static double lineAngle( double x1, double y1, double x2, double y2 );
static double linePerpendicularAngle( double x1, double y1, double x2, double y2 );
static double averageAngle( double x1, double y1, double x2, double y2, double x3, double y3 );
static double averageAngle( double a1, double a2 );
/** Parses a WKT block of the format "TYPE( contents )" and returns a pair of geometry type to contents ("Pair(wkbType, "contents")")
*/
static QPair<QgsWKBTypes::Type, QString> wktReadBlock( const QString& wkt );

View File

@ -500,3 +500,34 @@ void QgsLineStringV2::close()
}
addVertex( startPoint() );
}
double QgsLineStringV2::vertexAngle( const QgsVertexId& vertex ) const
{
if ( vertex.vertex == 0 || vertex.vertex >= ( numPoints() - 1 ) )
{
if ( isClosed() )
{
QPointF previous = mCoords[numPoints() - 1 ];
QPointF current = mCoords[0];
QPointF after = mCoords[1];
return QgsGeometryUtils::averageAngle( previous.x(), previous.y(), current.x(), current.y(), after.x(), after.y() );
}
else if ( vertex.vertex == 0 )
{
return QgsGeometryUtils::linePerpendicularAngle( mCoords[0].x(), mCoords[0].y(), mCoords[1].x(), mCoords[1].y() );
}
else
{
int a = numPoints() - 2;
int b = numPoints() - 1;
return QgsGeometryUtils::linePerpendicularAngle( mCoords[a].x(), mCoords[a].y(), mCoords[b].x(), mCoords[b].y() );
}
}
else
{
QPointF previous = mCoords[vertex.vertex - 1 ];
QPointF current = mCoords[vertex.vertex];
QPointF after = mCoords[vertex.vertex + 1];
return QgsGeometryUtils::averageAngle( previous.x(), previous.y(), current.x(), current.y(), after.x(), after.y() );
}
}

View File

@ -90,6 +90,10 @@ class CORE_EXPORT QgsLineStringV2: public QgsCurveV2
/** Appends first point if not already closed*/
void close();
/** Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments.
@return rotation in radians, clockwise from north*/
double vertexAngle( const QgsVertexId& vertex ) const override;
private:
QPolygonF mCoords;
QVector<double> mZ;

View File

@ -85,6 +85,9 @@ class CORE_EXPORT QgsPointV2: public QgsAbstractGeometryV2
double closestSegment( const QgsPointV2& pt, QgsPointV2& segmentPt, QgsVertexId& vertexAfter, bool* leftOf, double epsilon ) const override;
bool nextVertex( QgsVertexId& id, QgsPointV2& vertex ) const override;
/** Angle undefined. Always returns 0.0*/
double vertexAngle( const QgsVertexId& vertex ) const override { Q_UNUSED( vertex ); return 0.0; }
private:
double mX;
double mY;

View File

@ -141,7 +141,9 @@ class CORE_EXPORT QgsRenderContext
*/
const QgsExpressionContext& expressionContext() const { return mExpressionContext; }
/** Returns pointer to the unsegmentized geometry*/
const QgsAbstractGeometryV2* geometry() const { return mGeometry; }
/** Sets pointer to original (unsegmentized) geometry*/
void setGeometry( const QgsAbstractGeometryV2* geometry ) { mGeometry = geometry; }
private:

View File

@ -999,7 +999,7 @@ void QgsMarkerLineSymbolLayerV2::renderPolylineVertex( const QPolygonF& points,
offsetAlongLine *= QgsSymbolLayerV2Utils::lineWidthScaleFactor( rc, mOffsetAlongLineUnit, mOffsetAlongLineMapUnitScale );
}
if ( !mRotateMarker && offsetAlongLine == 0 && context.renderContext().geometry()
if ( offsetAlongLine == 0 && context.renderContext().geometry()
&& context.renderContext().geometry()->hasCurvedSegments() && ( placement == Vertex || placement == CurvePoint ) )
{
const QgsCoordinateTransform* ct = context.renderContext().coordinateTransform();
@ -1024,7 +1024,8 @@ void QgsMarkerLineSymbolLayerV2::renderPolylineVertex( const QPolygonF& points,
if ( mRotateMarker )
{
//todo: function to calculate angle...
double angle = context.renderContext().geometry()->vertexAngle( vId );
mMarker->setAngle( angle * 180 / M_PI );
}
mMarker->renderPoint( mapPoint, context.feature(), rc, -1, context.selected() );
}
@ -1042,13 +1043,17 @@ void QgsMarkerLineSymbolLayerV2::renderPolylineVertex( const QPolygonF& points,
i = points.count() - 1;
maxCount = points.count();
}
else
else if ( placement == Vertex )
{
i = 0;
maxCount = points.count();
if ( points.first() == points.last() )
isRing = true;
}
else
{
return;
}
if ( offsetAlongLine > 0 && ( placement == FirstVertex || placement == LastVertex ) )
{

View File

@ -31,6 +31,12 @@ class TestQgsGeometryUtils: public QObject
void testSegmentMidPoint();
void testCircleLength_data();
void testCircleLength();
void testLineAngle_data();
void testLineAngle();
void testLinePerpendicularAngle_data();
void testLinePerpendicularAngle();
void testAverageAngle_data();
void testAverageAngle();
};
void TestQgsGeometryUtils::testLeftOfLine_data()
@ -170,6 +176,90 @@ void TestQgsGeometryUtils::testCircleLength()
QVERIFY( qgsDoubleNear( expected, QgsGeometryUtils::circleLength( x1, y1, x2, y2, x3, y3 ) ) );
}
void TestQgsGeometryUtils::testLineAngle_data()
{
QTest::addColumn<double>( "x1" );
QTest::addColumn<double>( "y1" );
QTest::addColumn<double>( "x2" );
QTest::addColumn<double>( "y2" );
QTest::addColumn<double>( "expected" );
QTest::newRow( "lineAngle1" ) << 0.0 << 0.0 << 10.0 << 10.0 << 45.0;
QTest::newRow( "lineAngle2" ) << 0.0 << 0.0 << 10.0 << 0.0 << 90.0;
QTest::newRow( "lineAngle3" ) << 0.0 << 0.0 << 10.0 << -10.0 << 135.0;
QTest::newRow( "lineAngle4" ) << 0.0 << 0.0 << 0.0 << -10.0 << 180.0;
QTest::newRow( "lineAngle5" ) << 0.0 << 0.0 << -10.0 << -10.0 << 225.0;
QTest::newRow( "lineAngle6" ) << 0.0 << 0.0 << -10.0 << 0.0 << 270.0;
QTest::newRow( "lineAngle7" ) << 0.0 << 0.0 << -10.0 << 10.0 << 315.0;
QTest::newRow( "lineAngle8" ) << 0.0 << 0.0 << 0.0 << 10.0 << 0.0;
}
void TestQgsGeometryUtils::testLineAngle()
{
QFETCH( double, x1 );
QFETCH( double, y1 );
QFETCH( double, x2 );
QFETCH( double, y2 );
QFETCH( double, expected );
double lineAngle = QgsGeometryUtils::lineAngle( x1, y1, x2, y2 ) * 180 / M_PI;
QVERIFY( qgsDoubleNear( lineAngle, expected ) );
}
void TestQgsGeometryUtils::testLinePerpendicularAngle_data()
{
QTest::addColumn<double>( "x1" );
QTest::addColumn<double>( "y1" );
QTest::addColumn<double>( "x2" );
QTest::addColumn<double>( "y2" );
QTest::addColumn<double>( "expected" );
QTest::newRow( "lineAngle1" ) << 0.0 << 0.0 << 10.0 << 10.0 << 135.0;
QTest::newRow( "lineAngle2" ) << 0.0 << 0.0 << 10.0 << 0.0 << 180.0;
QTest::newRow( "lineAngle3" ) << 0.0 << 0.0 << 10.0 << -10.0 << 225.0;
QTest::newRow( "lineAngle4" ) << 0.0 << 0.0 << 0.0 << -10.0 << 270.0;
QTest::newRow( "lineAngle5" ) << 0.0 << 0.0 << -10.0 << -10.0 << 315.0;
QTest::newRow( "lineAngle6" ) << 0.0 << 0.0 << -10.0 << 0.0 << 0.0;
QTest::newRow( "lineAngle7" ) << 0.0 << 0.0 << -10.0 << 10.0 << 45.0;
QTest::newRow( "lineAngle8" ) << 0.0 << 0.0 << 0.0 << 10.0 << 90.0;
}
void TestQgsGeometryUtils::testLinePerpendicularAngle()
{
QFETCH( double, x1 );
QFETCH( double, y1 );
QFETCH( double, x2 );
QFETCH( double, y2 );
QFETCH( double, expected );
double pAngle = QgsGeometryUtils::linePerpendicularAngle( x1, y1, x2, y2 ) * 180 / M_PI;
QVERIFY( qgsDoubleNear( pAngle, expected ) );
}
void TestQgsGeometryUtils::testAverageAngle_data()
{
QTest::addColumn<double>( "angle1" );
QTest::addColumn<double>( "angle2" );
QTest::addColumn<double>( "expected" );
QTest::newRow( "testAverage1" ) << 45.0 << 135.0 << 90.0;
QTest::newRow( "testAverage2" ) << 315.0 << 45.0 << 0.0;
QTest::newRow( "testAverage3" ) << 45.0 << 315.0 << 0.0;
QTest::newRow( "testAverage4" ) << 315.0 << 270.0 << 292.5;
QTest::newRow( "testAverage5" ) << 140.0 << 240.0 << 190.0;
QTest::newRow( "testAverage6" ) << 240.0 << 140.0 << 190.0;
}
void TestQgsGeometryUtils::testAverageAngle()
{
QFETCH( double, angle1 );
QFETCH( double, angle2 );
QFETCH( double, expected );
double averageAngle = QgsGeometryUtils::averageAngle( angle1 * M_PI / 180.0, angle2 * M_PI / 180.0 ) * 180.0 / M_PI;
QVERIFY( qgsDoubleNear( averageAngle, expected, 0.0000000001 ) );
}
QTEST_MAIN( TestQgsGeometryUtils )
#include "testqgsgeometryutils.moc"