Skip to content

Commit 48c9539

Browse files
committed
Refactor curveToLine to emit equidistant segments and fix some issues
Fixes #16717 Fixes #16722 Include tests
1 parent 07a570f commit 48c9539

File tree

5 files changed

+241
-21
lines changed

5 files changed

+241
-21
lines changed

src/core/geometry/qgscircularstring.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "qgsmaptopixel.h"
2424
#include "qgspoint.h"
2525
#include "qgswkbptr.h"
26+
#include "qgslogger.h"
2627
#include <QPainter>
2728
#include <QPainterPath>
2829

@@ -353,6 +354,7 @@ QgsLineString *QgsCircularString::curveToLine( double tolerance, SegmentationTol
353354
QgsPointSequence points;
354355
int nPoints = numPoints();
355356

357+
QgsDebugMsg( QString( "curveToLine input: %1" ) .arg( asWkt( 2 ) ) );
356358
for ( int i = 0; i < ( nPoints - 2 ) ; i += 2 )
357359
{
358360
QgsGeometryUtils::segmentizeArc( pointN( i ), pointN( i + 1 ), pointN( i + 2 ), points, tolerance, toleranceType, is3D(), isMeasure() );

src/core/geometry/qgsgeometryutils.cpp

Lines changed: 118 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ email : marco.hugentobler at sourcepole dot com
2020
#include "qgsgeometrycollection.h"
2121
#include "qgslinestring.h"
2222
#include "qgswkbptr.h"
23+
#include "qgslogger.h"
2324

2425
#include <memory>
2526
#include <QStringList>
@@ -631,25 +632,44 @@ double QgsGeometryUtils::circleTangentDirection( const QgsPoint &tangentPoint, c
631632

632633
void QgsGeometryUtils::segmentizeArc( const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, QgsPointSequence &points, double tolerance, QgsAbstractGeometry::SegmentationToleranceType toleranceType, bool hasZ, bool hasM )
633634
{
635+
bool reversed = false;
634636
bool clockwise = false;
635637
int segSide = segmentSide( p1, p3, p2 );
636638
if ( segSide == -1 )
637639
{
638640
clockwise = true;
639641
}
640642

641-
QgsPoint circlePoint1 = clockwise ? p3 : p1;
642-
QgsPoint circlePoint2 = p2;
643-
QgsPoint circlePoint3 = clockwise ? p1 : p3 ;
643+
QgsPoint circlePoint1;
644+
const QgsPoint circlePoint2 = p2;
645+
QgsPoint circlePoint3;
646+
647+
if ( clockwise )
648+
{
649+
// Reverse !
650+
circlePoint1 = p3;
651+
circlePoint3 = p1;
652+
clockwise = false;
653+
reversed = true;
654+
}
655+
else
656+
{
657+
circlePoint1 = p1;
658+
circlePoint3 = p3;
659+
}
644660

645661
//adapted code from PostGIS
646662
double radius = 0;
647663
double centerX = 0;
648664
double centerY = 0;
649665
circleCenterRadius( circlePoint1, circlePoint2, circlePoint3, radius, centerX, centerY );
650666

667+
QgsDebugMsg( QString( "Center: POINT(%1 %2) - Radius: %3 - Clockwise: %4" )
668+
.arg( centerX ) .arg( centerY ) .arg( radius ) .arg( clockwise ) );
669+
651670
if ( circlePoint1 != circlePoint3 && ( radius < 0 || qgsDoubleNear( segSide, 0.0 ) ) ) //points are colinear
652671
{
672+
QgsDebugMsg( QString( "Collinear curve" ) );
653673
points.append( p1 );
654674
points.append( p2 );
655675
points.append( p3 );
@@ -662,24 +682,65 @@ void QgsGeometryUtils::segmentizeArc( const QgsPoint &p1, const QgsPoint &p2, co
662682
double halfAngle = std::acos( -tolerance / radius + 1 );
663683
increment = 2 * halfAngle;
664684
}
685+
QgsDebugMsg( QString( "Increment: %1" ).arg( increment ) );
665686

666687
//angles of pt1, pt2, pt3
667688
double a1 = std::atan2( circlePoint1.y() - centerY, circlePoint1.x() - centerX );
668689
double a2 = std::atan2( circlePoint2.y() - centerY, circlePoint2.x() - centerX );
669690
double a3 = std::atan2( circlePoint3.y() - centerY, circlePoint3.x() - centerX );
670691

671-
/* Adjust a3 up so we can increment from a1 to a3 cleanly */
672-
if ( a3 <= a1 )
673-
a3 += 2.0 * M_PI;
674-
if ( a2 < a1 )
675-
a2 += 2.0 * M_PI;
692+
QgsDebugMsg( QString( "a1:%1 (%4) a2:%2 (%5) a3:%3 (%6)" )
693+
.arg( a1 ).arg( a2 ).arg( a3 )
694+
.arg( a1 * 180 / M_PI ).arg( a2 * 180 / M_PI ).arg( a3 * 180 / M_PI )
695+
);
696+
697+
// Make segmentation symmetric
698+
const bool symmetric = true;
699+
if ( symmetric )
700+
{
701+
double angle = clockwise ? a1 - a3 : a3 - a1;
702+
if ( angle < 0 ) angle += M_PI * 2;
703+
QgsDebugMsg( QString( "total angle: %1 (%2)" )
704+
.arg( angle ) .arg( angle * 180 / M_PI )
705+
);
706+
707+
/* Number of segments in output */
708+
int segs = ceil( angle / increment );
709+
/* Tweak increment to be regular for all the arc */
710+
increment = angle / segs;
711+
712+
QgsDebugMsg( QString( "symmetric adjusted increment:%1" ) .arg( increment ) );
713+
}
714+
715+
if ( clockwise )
716+
{
717+
increment *= -1;
718+
/* Adjust a3 down so we can increment from a1 to a3 cleanly */
719+
if ( a3 > a1 )
720+
a3 -= 2.0 * M_PI;
721+
if ( a2 > a1 )
722+
a2 -= 2.0 * M_PI;
723+
}
724+
else
725+
{
726+
/* Adjust a3 up so we can increment from a1 to a3 cleanly */
727+
if ( a3 < a1 )
728+
a3 += 2.0 * M_PI;
729+
if ( a2 < a1 )
730+
a2 += 2.0 * M_PI;
731+
}
732+
733+
QgsDebugMsg( QString( "ADJUSTED - a1:%1 (%4) a2:%2 (%5) a3:%3 (%6)" )
734+
.arg( a1 ).arg( a2 ).arg( a3 )
735+
.arg( a1 * 180 / M_PI ).arg( a2 * 180 / M_PI ).arg( a3 * 180 / M_PI )
736+
);
676737

677738
double x, y;
678739
double z = 0;
679740
double m = 0;
680741

681742
QList<QgsPoint> stringPoints;
682-
stringPoints.insert( clockwise ? 0 : stringPoints.size(), circlePoint1 );
743+
stringPoints.insert( 0, circlePoint1 );
683744
if ( circlePoint2 != circlePoint3 && circlePoint1 != circlePoint2 ) //draw straight line segment if two points have the same position
684745
{
685746
QgsWkbTypes::Type pointWkbType = QgsWkbTypes::Point;
@@ -688,30 +749,58 @@ void QgsGeometryUtils::segmentizeArc( const QgsPoint &p1, const QgsPoint &p2, co
688749
if ( hasM )
689750
pointWkbType = QgsWkbTypes::addM( pointWkbType );
690751

752+
QgsDebugMsg( QString( "a1:%1 (%2), a3:%3 (%4), inc:%5, shi:?, cw:%6" )
753+
. arg( a1 ). arg( a1 * 180 / M_PI )
754+
. arg( a3 ). arg( a3 * 180 / M_PI )
755+
. arg( increment ). arg( clockwise )
756+
);
757+
691758
//make sure the curve point p2 is part of the segmentized vertices. But only if p1 != p3
759+
// TODO: make this a parameter
692760
bool addP2 = true;
693761
if ( qgsDoubleNear( circlePoint1.x(), circlePoint3.x() ) && qgsDoubleNear( circlePoint1.y(), circlePoint3.y() ) )
694762
{
695763
addP2 = false;
696764
}
697-
698-
for ( double angle = a1 + increment; angle < a3; angle += increment )
765+
addP2 = false;
766+
767+
// As we're adding the last point in any case, we'll avoid
768+
// including a point which is at less than 1% increment distance
769+
// from it (may happen to find them due to numbers approximation).
770+
// NOTE that this effectively allows in output some segments which
771+
// are more distant than requested. This is at most 1% off
772+
// from requested MaxAngle and less for MaxError.
773+
double tolError = increment / 100;
774+
double stopAngle = clockwise ? a3 - tolError : a3 - tolError;
775+
QgsDebugMsg( QString( "stopAngle: %1 (%2)" ) . arg( stopAngle ) .arg( stopAngle * 180 / M_PI ) );
776+
for ( double angle = a1 + increment; clockwise ? angle > stopAngle : angle < stopAngle; angle += increment )
699777
{
700-
if ( ( addP2 && angle > a2 ) )
778+
if ( addP2 && angle > a2 )
701779
{
702-
stringPoints.insert( clockwise ? 0 : stringPoints.size(), circlePoint2 );
780+
if ( clockwise )
781+
{
782+
if ( *stringPoints.begin() != circlePoint2 )
783+
{
784+
QgsDebugMsg( QString( "Adding control point, with angle %1 (%2)" ) . arg( a2 ) .arg( a2 * 180 / M_PI ) );
785+
stringPoints.insert( 0, circlePoint2 );
786+
}
787+
}
788+
else
789+
{
790+
if ( *stringPoints.rbegin() != circlePoint2 )
791+
{
792+
QgsDebugMsg( QString( "Adding control point, with angle %1 (%2)" ) . arg( a2 ) .arg( a2 * 180 / M_PI ) );
793+
stringPoints.insert( stringPoints.size(), circlePoint2 );
794+
}
795+
}
703796
addP2 = false;
704797
}
705798

799+
QgsDebugMsg( QString( "SA - %1 (%2)" ) . arg( angle ) .arg( angle * 180 / M_PI ) );
800+
706801
x = centerX + radius * std::cos( angle );
707802
y = centerY + radius * std::sin( angle );
708803

709-
if ( !hasZ && !hasM )
710-
{
711-
stringPoints.insert( clockwise ? 0 : stringPoints.size(), QgsPoint( x, y ) );
712-
continue;
713-
}
714-
715804
if ( hasZ )
716805
{
717806
z = interpolateArcValue( angle, a1, a2, a3, circlePoint1.z(), circlePoint2.z(), circlePoint3.z() );
@@ -721,10 +810,18 @@ void QgsGeometryUtils::segmentizeArc( const QgsPoint &p1, const QgsPoint &p2, co
721810
m = interpolateArcValue( angle, a1, a2, a3, circlePoint1.m(), circlePoint2.m(), circlePoint3.m() );
722811
}
723812

724-
stringPoints.insert( clockwise ? 0 : stringPoints.size(), QgsPoint( pointWkbType, x, y, z, m ) );
813+
QgsDebugMsg( QString( " -> POINT(%1 %2)" ) . arg( x ) .arg( y ) );
814+
stringPoints.insert( stringPoints.size(), QgsPoint( pointWkbType, x, y, z, m ) );
725815
}
726816
}
727-
stringPoints.insert( clockwise ? 0 : stringPoints.size(), circlePoint3 );
817+
QgsDebugMsg( QString( " appending last point -> POINT(%1 %2)" ) . arg( circlePoint3.x() ) .arg( circlePoint3.y() ) );
818+
stringPoints.insert( stringPoints.size(), circlePoint3 );
819+
820+
// TODO: check if or implement QgsPointSequence directly taking an iterator to append
821+
if ( reversed )
822+
{
823+
std::reverse( stringPoints.begin(), stringPoints.end() );
824+
}
728825
points.append( stringPoints );
729826
}
730827

tests/src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ SET(TESTS
100100
testcontrastenhancements.cpp
101101
testqgscoordinatereferencesystem.cpp
102102
testqgscoordinatetransform.cpp
103+
testqgscurve.cpp
103104
testqgsdatadefinedsizelegend.cpp
104105
testqgsdataitem.cpp
105106
testqgsdatasourceuri.cpp

tests/src/core/testqgscurve.cpp

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/***************************************************************************
2+
testqgscurve.cpp
3+
--------------------------------------
4+
Date : 21 July 2017
5+
Copyright : (C) 2017 by Sandro Santilli
6+
Email : strk @ kbt.io
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
#include <QObject>
16+
#include <QString>
17+
#include <QApplication>
18+
#include <memory> // for unique_ptr
19+
20+
//qgis includes...
21+
#include "qgsabstractgeometry.h"
22+
#include "qgscircularstring.h"
23+
#include "qgsgeometry.h"
24+
#include "qgsgeometryfactory.h"
25+
#include "qgslinestring.h"
26+
#include "qgspoint.h"
27+
#include "qgstest.h"
28+
#include "qgstestutils.h"
29+
30+
/** \ingroup UnitTests
31+
* This is a unit test for the operations on curve geometries
32+
*/
33+
class TestQgsCurve : public QObject
34+
{
35+
Q_OBJECT
36+
37+
public:
38+
TestQgsCurve() {};
39+
40+
private slots:
41+
//void initTestCase();// will be called before the first testfunction is executed.
42+
//void cleanupTestCase();// will be called after the last testfunction was executed.
43+
//void init();// will be called before each testfunction is executed.
44+
//void cleanup();// will be called after every testfunction.
45+
46+
void curveToLine();
47+
};
48+
49+
50+
#define TEST_C2L(circularString, tol, toltype, exp, prec) { \
51+
std::unique_ptr< QgsLineString > lineString( \
52+
circularString->curveToLine(tol, toltype) \
53+
); \
54+
QVERIFY( lineString.get() ); \
55+
QString wkt_out = lineString->asWkt(prec); \
56+
QCOMPARE( wkt_out, QString( exp ) ); \
57+
/* Test reverse */ \
58+
std::unique_ptr< QgsCircularString > reversed( \
59+
circularString->reversed() \
60+
); \
61+
lineString.reset( \
62+
reversed->curveToLine(tol, toltype) \
63+
); \
64+
wkt_out = lineString->asWkt(prec); \
65+
lineString.reset( \
66+
reversed->curveToLine(tol, toltype) \
67+
); \
68+
std::unique_ptr< QgsLineString > expgeom( \
69+
dynamic_cast<QgsLineString *>( \
70+
QgsGeometryFactory::geomFromWkt( exp ) \
71+
) \
72+
); \
73+
expgeom.reset( expgeom->reversed() ); \
74+
QString exp_reversed = expgeom->asWkt(prec); \
75+
QCOMPARE( wkt_out, exp_reversed ); \
76+
}
77+
78+
void TestQgsCurve::curveToLine()
79+
{
80+
std::unique_ptr< QgsCircularString > circularString;
81+
82+
/* input: 2 quadrants arc (180 degrees, PI radians) */
83+
circularString.reset( dynamic_cast<QgsCircularString *>(
84+
QgsGeometryFactory::geomFromWkt( QString(
85+
"CIRCULARSTRING(0 0,100 100,200 0)"
86+
) )
87+
) );
88+
QVERIFY( circularString.get() );
89+
90+
/* op: Maximum of 10 units of difference, symmetric */
91+
TEST_C2L( circularString, 10, QgsAbstractGeometry::MaximumDifference,
92+
"LineString (0 0, 29.29 70.71, 100 100, 170.71 70.71, 200 0)", 2 );
93+
94+
/* op: Maximum of M_PI / 8 degrees of angle, (a)symmetric */
95+
/* See https://issues.qgis.org/issues/16717 */
96+
TEST_C2L( circularString, M_PI / 8, QgsAbstractGeometry::MaximumAngle,
97+
"LineString (0 0, 7.61 38.27, 29.29 70.71, 61.73 92.39, 100 100, 138.27 92.39, 170.71 70.71, 192.39 38.27, 200 0)", 2 );
98+
99+
/* op: Maximum of 70 degrees of angle, symmetric */
100+
/* See https://issues.qgis.org/issues/16722 */
101+
TEST_C2L( circularString, 70 * M_PI / 180, QgsAbstractGeometry::MaximumAngle,
102+
"LineString (0 0, 50 86.6, 150 86.6, 200 0)", 2 );
103+
104+
/* input: 2 arcs of 2 quadrants each (180 degrees + 180 degrees other direction) */
105+
circularString.reset( dynamic_cast<QgsCircularString *>(
106+
QgsGeometryFactory::geomFromWkt( QString(
107+
"CIRCULARSTRING(0 0,100 100,200 0,300 -100,400 0)"
108+
) )
109+
) );
110+
QVERIFY( circularString.get() );
111+
112+
/* op: Maximum of M_PI / 3 degrees of angle */
113+
TEST_C2L( circularString, M_PI / 3, QgsAbstractGeometry::MaximumAngle,
114+
"LineString (0 0, 50 86.6, 150 86.6, 200 0, 200 0, 250 -86.6, 350 -86.6, 400 0)", 2 );
115+
}
116+
117+
118+
QGSTEST_MAIN( TestQgsCurve )
119+
#include "testqgscurve.moc"

tests/src/core/testqgsgeometry.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5319,6 +5319,7 @@ void TestQgsGeometry::directionNeutralSegmentation()
53195319
QgsLineString *CCWLineString = CCWCircularString->curveToLine();
53205320
QgsLineString *reversedCCWLineString = CCWLineString->reversed();
53215321

5322+
QCOMPARE( CWLineString->asWkt(), reversedCCWLineString->asWkt() );
53225323
bool equal = ( *CWLineString == *reversedCCWLineString );
53235324

53245325
delete CWCircularString;

0 commit comments

Comments
 (0)