[FEATURE][processing] Add "convert to curves" algorithm

Converts a linear geometry type to the corresponding curved
geometry type, attemping to identify segments in the original
geometries which can be replaced by arcs.

The distance tolerance parameter specifies the maximum distance
allowed between the original location of vertices and where they
would fall on the converted curved geometries.

This algorithm only consider a segments as suitable for
replacing with an arc if the points are all regularly spaced on
the candidate arc. The angle tolerance parameter specifies the
maximum angular deviation (in degrees) allowed when testing for
regular point spacing.

Already curved geometries will be retained without change.
This commit is contained in:
Nyall Dawson 2018-03-05 12:36:35 +10:00 committed by olivierdalang
parent 27b5dae4bf
commit 9939142ba9
12 changed files with 651 additions and 1 deletions

View File

@ -39,6 +39,7 @@ SET(QGIS_ANALYSIS_SRCS
processing/qgsalgorithmclip.cpp
processing/qgsalgorithmconditionalbranch.cpp
processing/qgsalgorithmconstantraster.cpp
processing/qgsalgorithmconverttocurves.cpp
processing/qgsalgorithmconvexhull.cpp
processing/qgsalgorithmdbscanclustering.cpp
processing/qgsalgorithmdeleteduplicategeometries.cpp

View File

@ -0,0 +1,157 @@
/***************************************************************************
qgsalgorithmconverttocurves.cpp
---------------------
begin : March 2018
copyright : (C) 2018 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsalgorithmconverttocurves.h"
///@cond PRIVATE
QString QgsConvertToCurvesAlgorithm::name() const
{
return QStringLiteral( "converttocurves" );
}
QString QgsConvertToCurvesAlgorithm::displayName() const
{
return QObject::tr( "Convert to curved geometries" );
}
QStringList QgsConvertToCurvesAlgorithm::tags() const
{
return QObject::tr( "straight,segmentize,curves,curved,circular" ).split( ',' );
}
QString QgsConvertToCurvesAlgorithm::group() const
{
return QObject::tr( "Vector geometry" );
}
QString QgsConvertToCurvesAlgorithm::groupId() const
{
return QStringLiteral( "vectorgeometry" );
}
QString QgsConvertToCurvesAlgorithm::outputName() const
{
return QObject::tr( "Curves" );
}
QString QgsConvertToCurvesAlgorithm::shortHelpString() const
{
return QObject::tr( "This algorithm converts a geometry into its curved geometry equivalent.\n\n"
"Already curved geometries will be retained without change." );
}
QgsConvertToCurvesAlgorithm *QgsConvertToCurvesAlgorithm::createInstance() const
{
return new QgsConvertToCurvesAlgorithm();
}
QList<int> QgsConvertToCurvesAlgorithm::inputLayerTypes() const
{
return QList<int>() << QgsProcessing::TypeVectorLine << QgsProcessing::TypeVectorPolygon;
}
void QgsConvertToCurvesAlgorithm::initParameters( const QVariantMap & )
{
std::unique_ptr< QgsProcessingParameterNumber > tolerance = qgis::make_unique< QgsProcessingParameterNumber >( QStringLiteral( "DISTANCE" ),
QObject::tr( "Maximum distance tolerance" ), QgsProcessingParameterNumber::Double,
0.000001, false, 0, 10000000.0 );
tolerance->setIsDynamic( true );
tolerance->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "DISTANCE" ), QObject::tr( "Maximum distance tolerance" ), QgsPropertyDefinition::DoublePositive ) );
tolerance->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) );
addParameter( tolerance.release() );
std::unique_ptr< QgsProcessingParameterNumber > angleTolerance = qgis::make_unique< QgsProcessingParameterNumber >( QStringLiteral( "ANGLE" ),
QObject::tr( "Maximum angle tolerance" ), QgsProcessingParameterNumber::Double,
0.000001, false, 0, 45.0 );
angleTolerance->setIsDynamic( true );
angleTolerance->setDynamicPropertyDefinition( QgsPropertyDefinition( QStringLiteral( "ANGLE" ), QObject::tr( "Maximum angle tolerance" ), QgsPropertyDefinition::DoublePositive ) );
angleTolerance->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) );
addParameter( angleTolerance.release() );
}
bool QgsConvertToCurvesAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback * )
{
mTolerance = parameterAsDouble( parameters, QStringLiteral( "DISTANCE" ), context );
mDynamicTolerance = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "DISTANCE" ) );
if ( mDynamicTolerance )
mToleranceProperty = parameters.value( QStringLiteral( "DISTANCE" ) ).value< QgsProperty >();
mAngleTolerance = parameterAsDouble( parameters, QStringLiteral( "ANGLE" ), context );
mDynamicAngleTolerance = QgsProcessingParameters::isDynamic( parameters, QStringLiteral( "ANGLE" ) );
if ( mDynamicAngleTolerance )
mAngleToleranceProperty = parameters.value( QStringLiteral( "ANGLE" ) ).value< QgsProperty >();
return true;
}
QgsFeatureList QgsConvertToCurvesAlgorithm::processFeature( const QgsFeature &feature, QgsProcessingContext &context, QgsProcessingFeedback * )
{
QgsFeature f = feature;
if ( f.hasGeometry() )
{
QgsGeometry geometry = f.geometry();
double tolerance = mTolerance;
if ( mDynamicTolerance )
tolerance = mToleranceProperty.valueAsDouble( context.expressionContext(), tolerance );
double angleTolerance = mAngleTolerance;
if ( mDynamicAngleTolerance )
angleTolerance = mAngleToleranceProperty.valueAsDouble( context.expressionContext(), angleTolerance );
f.setGeometry( geometry.convertToCurves( tolerance, angleTolerance * M_PI / 180.0 ) );
}
return QgsFeatureList() << f;
}
QgsWkbTypes::Type QgsConvertToCurvesAlgorithm::outputWkbType( QgsWkbTypes::Type inputWkbType ) const
{
if ( QgsWkbTypes::isCurvedType( inputWkbType ) )
return inputWkbType;
QgsWkbTypes::Type outType = QgsWkbTypes::Unknown;
switch ( QgsWkbTypes::geometryType( inputWkbType ) )
{
case QgsWkbTypes::PointGeometry:
case QgsWkbTypes::NullGeometry:
case QgsWkbTypes::UnknownGeometry:
return inputWkbType;
case QgsWkbTypes::LineGeometry:
outType = QgsWkbTypes::CompoundCurve;
break;
case QgsWkbTypes::PolygonGeometry:
outType = QgsWkbTypes::CurvePolygon;
break;
}
if ( QgsWkbTypes::isMultiType( inputWkbType ) )
outType = QgsWkbTypes::multiType( outType );
if ( QgsWkbTypes::hasZ( inputWkbType ) )
outType = QgsWkbTypes::addZ( outType );
if ( QgsWkbTypes::hasM( inputWkbType ) )
outType = QgsWkbTypes::addM( outType );
return outType;
}
///@endcond

View File

@ -0,0 +1,70 @@
/***************************************************************************
qgsalgorithmconverttocurves.h
---------------------
begin : March 2018
copyright : (C) 2018 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSALGORITHMSEGMENTIZE_H
#define QGSALGORITHMSEGMENTIZE_H
#define SIP_NO_FILE
#include "qgis.h"
#include "qgsprocessingalgorithm.h"
#include "qgsmaptopixelgeometrysimplifier.h"
///@cond PRIVATE
/**
* Native segmentize by maximum distance algorithm.
*/
class QgsConvertToCurvesAlgorithm : public QgsProcessingFeatureBasedAlgorithm
{
public:
QgsConvertToCurvesAlgorithm() = default;
QString name() const override;
QString displayName() const override;
QStringList tags() const override;
QString group() const override;
QString groupId() const override;
QString shortHelpString() const override;
QgsConvertToCurvesAlgorithm *createInstance() const override SIP_FACTORY;
QList<int> inputLayerTypes() const override;
void initParameters( const QVariantMap &configuration = QVariantMap() ) override;
protected:
QString outputName() const override;
bool prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
QgsFeatureList processFeature( const QgsFeature &feature, QgsProcessingContext &, QgsProcessingFeedback *feedback ) override;
QgsWkbTypes::Type outputWkbType( QgsWkbTypes::Type inputWkbType ) const override;
private:
double mTolerance = 0.000001;
bool mDynamicTolerance = false;
QgsProperty mToleranceProperty;
double mAngleTolerance = 0.000001;
bool mDynamicAngleTolerance = false;
QgsProperty mAngleToleranceProperty;
};
///@endcond PRIVATE
#endif // QGSALGORITHMSEGMENTIZE_H

View File

@ -34,6 +34,7 @@
#include "qgsalgorithmclip.h"
#include "qgsalgorithmconditionalbranch.h"
#include "qgsalgorithmconstantraster.h"
#include "qgsalgorithmconverttocurves.h"
#include "qgsalgorithmconvexhull.h"
#include "qgsalgorithmdbscanclustering.h"
#include "qgsalgorithmdeleteduplicategeometries.h"
@ -231,6 +232,7 @@ void QgsNativeAlgorithms::loadAlgorithms()
addAlgorithm( new QgsCombineStylesAlgorithm() );
addAlgorithm( new QgsConditionalBranchAlgorithm() );
addAlgorithm( new QgsConstantRasterAlgorithm() );
addAlgorithm( new QgsConvertToCurvesAlgorithm() );
addAlgorithm( new QgsConvexHullAlgorithm() );
addAlgorithm( new QgsDbscanClusteringAlgorithm() );
addAlgorithm( new QgsDeleteDuplicateGeometriesAlgorithm() );

View File

@ -2129,6 +2129,13 @@ QgsGeometry QgsGeometry::densifyByDistance( double distance ) const
return engine.densifyByDistance( distance );
}
QgsGeometry QgsGeometry::convertToCurves( double distanceTolerance, double angleTolerance ) const
{
QgsInternalGeometryEngine engine( *this );
return engine.convertToCurves( distanceTolerance, angleTolerance );
}
QgsGeometry QgsGeometry::centroid() const
{
if ( !d->geometry )

View File

@ -1243,6 +1243,21 @@ class CORE_EXPORT QgsGeometry
*/
QgsGeometry densifyByDistance( double distance ) const;
/**
* Attempts to convert a non-curved geometry into a curved geometry type (e.g.
* LineString to CompoundCurve, Polygon to CurvePolygon).
*
* The \a distanceTolerance specifies the maximum deviation allowed between the original location
* of vertices and where they would fall on the candidate curved geometry.
*
* This method only consider a segments as suitable for replacing with an arc if the points are all
* regularly spaced on the candidate arc. The \a pointSpacingAngleTolerance parameter specifies the maximum
* angular deviation (in radians) allowed when testing for regular point spacing.
*
* \since QGIS 3.2
*/
QgsGeometry convertToCurves( double distanceTolerance = 1e-8, double angleTolerance = 1e-8 ) const;
/**
* Returns the center of mass of a geometry.
*

View File

@ -871,6 +871,66 @@ double QgsGeometryUtils::circleTangentDirection( const QgsPoint &tangentPoint, c
return angle;
}
// Ported from PostGIS' pt_continues_arc
bool QgsGeometryUtils::pointContinuesArc( const QgsPoint &a1, const QgsPoint &a2, const QgsPoint &a3, const QgsPoint &b, double distanceTolerance, double pointSpacingAngleTolerance )
{
double centerX = 0;
double centerY = 0;
double radius = 0;
circleCenterRadius( a1, a2, a3, radius, centerX, centerY );
// Co-linear a1/a2/a3
if ( radius < 0.0 )
return false;
// distance of candidate point to center of arc a1-a2-a3
double bDistance = std::sqrt( ( b.x() - centerX ) * ( b.x() - centerX ) +
( b.y() - centerY ) * ( b.y() - centerY ) );
double diff = std::fabs( radius - bDistance );
auto arcAngle = []( const QgsPoint & a, const QgsPoint & b, const QgsPoint & c )->double
{
double abX = b.x() - a.x();
double abY = b.y() - a.y();
double cbX = b.x() - c.x();
double cbY = b.y() - c.y();
double dot = ( abX * cbX + abY * cbY ); /* dot product */
double cross = ( abX * cbY - abY * cbX ); /* cross product */
double alpha = std::atan2( cross, dot );
return alpha;
};
// Is the point b on the circle?
if ( diff < distanceTolerance )
{
double angle1 = arcAngle( a1, a2, a3 );
double angle2 = arcAngle( a2, a3, b );
// Is the sweep angle similar to the previous one?
// We only consider a segment replacable by an arc if the points within
// it are regularly spaced
diff = std::fabs( angle1 - angle2 );
if ( diff > pointSpacingAngleTolerance )
{
return false;
}
int a2Side = leftOfLine( a2.x(), a2.y(), a1.x(), a1.y(), a3.x(), a3.y() );
int bSide = leftOfLine( b.x(), b.y(), a1.x(), a1.y(), a3.x(), a3.y() );
// Is the point b on the same side of a1/a3 as the mid-point a2 is?
// If not, it's in the unbounded part of the circle, so it continues the arc, return true.
if ( bSide != a2Side )
return true;
}
return false;
}
void QgsGeometryUtils::segmentizeArc( const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, QgsPointSequence &points, double tolerance, QgsAbstractGeometry::SegmentationToleranceType toleranceType, bool hasZ, bool hasM )
{
bool reversed = false;

View File

@ -262,7 +262,7 @@ class CORE_EXPORT QgsGeometryUtils
static QVector<SelfIntersection> selfIntersections( const QgsAbstractGeometry *geom, int part, int ring, double tolerance ) SIP_SKIP;
/**
* Returns a value < 0 if the point (\a x, \a y) is left of the line from (\a x1, \a y1) -> ( \a x2, \a y2).
* Returns a value < 0 if the point (\a x, \a y) is left of the line from (\a x1, \a y1) -> (\a x2, \a y2).
* A positive return value indicates the point is to the right of the line.
*
* If the return value is 0, then the test was unsuccessful (e.g. due to testing a point exactly
@ -372,6 +372,22 @@ class CORE_EXPORT QgsGeometryUtils
QgsAbstractGeometry::SegmentationToleranceType toleranceType = QgsAbstractGeometry::MaximumAngle,
bool hasZ = false, bool hasM = false );
/**
* Returns true if point \a b is on the arc formed by points \a a1, \a a2, and \a a3, but not within
* that arc portion already described by \a a1, \a a2 and \a a3.
*
* The \a distanceTolerance specifies the maximum deviation allowed between the original location
* of point \b and where it would fall on the candidate arc.
*
* This method only consider a segments as continuing an arc if the points are all regularly spaced
* on the candidate arc. The \a pointSpacingAngleTolerance parameter specifies the maximum
* angular deviation (in radians) allowed when testing for regular point spacing.
*
* \since QGIS 3.2
*/
static bool pointContinuesArc( const QgsPoint &a1, const QgsPoint &a2, const QgsPoint &a3, const QgsPoint &b, double distanceTolerance,
double pointSpacingAngleTolerance );
/**
* For line defined by points pt1 and pt3, find out on which side of the line is point pt3.
* Returns -1 if pt3 on the left side, 1 if pt3 is on the right side or 0 if pt3 lies on the line.

View File

@ -20,6 +20,7 @@
#include "qgsmultipolygon.h"
#include "qgspolygon.h"
#include "qgsmulticurve.h"
#include "qgscircularstring.h"
#include "qgsgeometry.h"
#include "qgsgeometryutils.h"
#include "qgslinesegment.h"
@ -1167,3 +1168,256 @@ QVector<QgsPointXY> QgsInternalGeometryEngine::randomPointsInPolygon( const QgsG
}
return result;
}
// ported from PostGIS' lwgeom pta_unstroke
std::unique_ptr< QgsCompoundCurve > lineToCurve( const QgsLineString *lineString, double distanceTolerance,
double pointSpacingAngleTolerance )
{
std::unique_ptr< QgsCompoundCurve > out = qgis::make_unique< QgsCompoundCurve >();
/* Minimum number of edges, per quadrant, required to define an arc */
const unsigned int minQuadEdges = 2;
/* Die on null input */
if ( !lineString )
return nullptr;
/* Null on empty input? */
if ( lineString->nCoordinates() == 0 )
return nullptr;
/* We can't desegmentize anything shorter than four points */
if ( lineString->nCoordinates() < 4 )
{
out->addCurve( lineString->clone() );
return out;
}
/* Allocate our result array of vertices that are part of arcs */
int numEdges = lineString->nCoordinates() - 1;
QVector< int > edgesInArcs( numEdges + 1, 0 );
auto arcAngle = []( const QgsPoint & a, const QgsPoint & b, const QgsPoint & c )->double
{
double abX = b.x() - a.x();
double abY = b.y() - a.y();
double cbX = b.x() - c.x();
double cbY = b.y() - c.y();
double dot = ( abX * cbX + abY * cbY ); /* dot product */
double cross = ( abX * cbY - abY * cbX ); /* cross product */
double alpha = std::atan2( cross, dot );
return alpha;
};
/* We make a candidate arc of the first two edges, */
/* And then see if the next edge follows it */
int i = 0;
int j = 0;
int k = 0;
int currentArc = 1;
QgsPoint a1;
QgsPoint a2;
QgsPoint a3;
QgsPoint b;
double centerX = 0.0;
double centerY = 0.0;
double radius = 0;
while ( i < numEdges - 2 )
{
unsigned int arcEdges = 0;
double numQuadrants = 0;
double angle;
bool foundArc = false;
/* Make candidate arc */
a1 = lineString->pointN( i );
a2 = lineString->pointN( i + 1 );
a3 = lineString->pointN( i + 2 );
QgsPoint first = a1;
for ( j = i + 3; j < numEdges + 1; j++ )
{
b = lineString->pointN( j );
/* Does this point fall on our candidate arc? */
if ( QgsGeometryUtils::pointContinuesArc( a1, a2, a3, b, distanceTolerance, pointSpacingAngleTolerance ) )
{
/* Yes. Mark this edge and the two preceding it as arc components */
foundArc = true;
for ( k = j - 1; k > j - 4; k-- )
edgesInArcs[k] = currentArc;
}
else
{
/* No. So we're done with this candidate arc */
currentArc++;
break;
}
a1 = a2;
a2 = a3;
a3 = b;
}
/* Jump past all the edges that were added to the arc */
if ( foundArc )
{
/* Check if an arc was composed by enough edges to be
* really considered an arc
* See http://trac.osgeo.org/postgis/ticket/2420
*/
arcEdges = j - 1 - i;
if ( first.x() == b.x() && first.y() == b.y() )
{
numQuadrants = 4;
}
else
{
QgsGeometryUtils::circleCenterRadius( first, b, a1, radius, centerX, centerY );
angle = arcAngle( first, QgsPoint( centerX, centerY ), b );
int p2Side = QgsGeometryUtils::leftOfLine( b.x(), b.y(), first.x(), first.y(), a1.x(), a1.y() );
if ( p2Side >= 0 )
angle = -angle;
if ( angle < 0 )
angle = 2 * M_PI + angle;
numQuadrants = ( 4 * angle ) / ( 2 * M_PI );
}
/* a1 is first point, b is last point */
if ( arcEdges < minQuadEdges * numQuadrants )
{
// LWDEBUGF( 4, "Not enough edges for a %g quadrants arc, %g needed", num_quadrants, min_quad_edges * num_quadrants );
for ( k = j - 1; k >= i; k-- )
edgesInArcs[k] = 0;
}
i = j - 1;
}
else
{
/* Mark this edge as a linear edge */
edgesInArcs[i] = 0;
i = i + 1;
}
}
int start = 0;
int end = 0;
/* non-zero if edge is part of an arc */
int edgeType = edgesInArcs[0];
auto addPointsToCurve = [ lineString, &out ]( int start, int end, int type )
{
if ( type == 0 )
{
// straight segment
QVector< QgsPoint > points;
for ( int j = start; j < end + 2; ++ j )
{
points.append( lineString->pointN( j ) );
}
std::unique_ptr< QgsCurve > straightSegment = qgis::make_unique< QgsLineString >( points );
out->addCurve( straightSegment.release() );
}
else
{
// curved segment
QVector< QgsPoint > points;
points.append( lineString->pointN( start ) );
points.append( lineString->pointN( ( start + end + 1 ) / 2 ) );
points.append( lineString->pointN( end + 1 ) );
std::unique_ptr< QgsCircularString > curvedSegment = qgis::make_unique< QgsCircularString >();
curvedSegment->setPoints( points );
out->addCurve( curvedSegment.release() );
}
};
for ( int i = 1; i < numEdges; i++ )
{
if ( edgeType != edgesInArcs[i] )
{
end = i - 1;
addPointsToCurve( start, end, edgeType );
start = i;
edgeType = edgesInArcs[i];
}
}
/* Roll out last item */
end = numEdges - 1;
addPointsToCurve( start, end, edgeType );
return out;
}
std::unique_ptr< QgsAbstractGeometry > convertGeometryToCurves( const QgsAbstractGeometry *geom, double distanceTolerance, double angleTolerance )
{
if ( QgsWkbTypes::geometryType( geom->wkbType() ) == QgsWkbTypes::LineGeometry )
{
return lineToCurve( static_cast< const QgsLineString * >( geom ), distanceTolerance, angleTolerance );
}
else
{
// polygon
const QgsPolygon *polygon = static_cast< const QgsPolygon * >( geom );
std::unique_ptr< QgsCurvePolygon > result = qgis::make_unique< QgsCurvePolygon>();
result->setExteriorRing( lineToCurve( static_cast< const QgsLineString * >( polygon->exteriorRing() ),
distanceTolerance, angleTolerance ).release() );
for ( int i = 0; i < polygon->numInteriorRings(); ++i )
{
result->addInteriorRing( lineToCurve( static_cast< const QgsLineString * >( polygon->interiorRing( i ) ),
distanceTolerance, angleTolerance ).release() );
}
return result;
}
}
QgsGeometry QgsInternalGeometryEngine::convertToCurves( double distanceTolerance, double angleTolerance ) const
{
if ( !mGeometry )
{
return QgsGeometry();
}
if ( QgsWkbTypes::geometryType( mGeometry->wkbType() ) == QgsWkbTypes::PointGeometry )
{
return QgsGeometry( mGeometry->clone() ); // point geometry, nothing to do
}
if ( QgsWkbTypes::isCurvedType( mGeometry->wkbType() ) )
{
// already curved. In future we may want to allow this, and convert additional candidate segments
// in an already curved geometry to curves
return QgsGeometry( mGeometry->clone() );
}
if ( const QgsGeometryCollection *gc = qgsgeometry_cast< const QgsGeometryCollection *>( mGeometry ) )
{
int numGeom = gc->numGeometries();
QVector< QgsAbstractGeometry * > geometryList;
geometryList.reserve( numGeom );
for ( int i = 0; i < numGeom; ++i )
{
geometryList << convertGeometryToCurves( gc->geometryN( i ), distanceTolerance, angleTolerance ).release();
}
QgsGeometry first = QgsGeometry( geometryList.takeAt( 0 ) );
for ( QgsAbstractGeometry *g : qgis::as_const( geometryList ) )
{
first.addPart( g );
}
return first;
}
else
{
return QgsGeometry( convertGeometryToCurves( mGeometry, distanceTolerance, angleTolerance ) );
}
}

View File

@ -166,6 +166,21 @@ class QgsInternalGeometryEngine
static QVector< QgsPointXY > randomPointsInPolygon( const QgsGeometry &polygon, int count,
const std::function< bool( const QgsPointXY & ) > &acceptPoint, unsigned long seed = 0, QgsFeedback *feedback = nullptr );
/**
* Attempts to convert a non-curved geometry into a curved geometry type (e.g.
* LineString to CompoundCurve, Polygon to CurvePolygon).
*
* The \a distanceTolerance specifies the maximum deviation allowed between the original location
* of vertices and where they would fall on the candidate curved geometry.
*
* This method only consider a segments as suitable for replacing with an arc if the points are all
* regularly spaced on the candidate arc. The \a pointSpacingAngleTolerance parameter specifies the maximum
* angular deviation (in radians) allowed when testing for regular point spacing.
*
* \since QGIS 3.2
*/
QgsGeometry convertToCurves( double distanceTolerance, double angleTolerance ) const;
private:
const QgsAbstractGeometry *mGeometry = nullptr;
};

View File

@ -80,6 +80,7 @@ class TestQgsGeometryUtils: public QObject
void testTriangleArea();
void testWeightedPointInTriangle_data();
void testWeightedPointInTriangle();
void testPointContinuesArc();
};
@ -1470,5 +1471,31 @@ void TestQgsGeometryUtils::testWeightedPointInTriangle()
QGSCOMPARENEAR( y, expectedY, 0.0000001 );
}
void TestQgsGeometryUtils::testPointContinuesArc()
{
// normal arcs
QVERIFY( QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 1, -1 ), 0.000000001, 0.000001 ) );
QVERIFY( QgsGeometryUtils::pointContinuesArc( QgsPoint( 2, 0 ), QgsPoint( 1, 1 ), QgsPoint( 0, 0 ), QgsPoint( 1, -1 ), 0.000000001, 0.000001 ) );
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 3, 0 ), 0.000000001, 0.000001 ) );
QVERIFY( QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 0.29289321881, 0.707106781 ), QgsPoint( 1, 1 ), QgsPoint( 1.707106781, 0.707106781 ), 0.00001, 0.00001 ) );
// irregular spacing
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 0.29289321881, 0.707106781 ), QgsPoint( 1, 1 ), QgsPoint( 1, -1 ), 0.00001, 0.00001 ) );
// inside current arc
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 0.29289321881, 0.707106781 ), QgsPoint( 1, 1 ), QgsPoint( 0.29289321881, 0.707106781 ), 0.00001, 0.00001 ) );
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 0.29289321881, 0.707106781 ), QgsPoint( 1, 1 ), QgsPoint( 1, 1 ), 0.00001, 0.00001 ) );
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 0.29289321881, 0.707106781 ), QgsPoint( 1, 1 ), QgsPoint( 0, 0 ), 0.00001, 0.00001 ) );
// colinear points
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 0.5, 0.5 ), QgsPoint( 1, 1 ), QgsPoint( 1.5, 1.5 ), 0.00001, 0.00001 ) );
// with a bit more tolerance
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 1.01, -1 ), 0.000000001, 0.05 ) );
QVERIFY( QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 1.01, -1 ), 0.1, 0.05 ) );
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 1.01, -1 ), 0.1, 0.000001 ) );
QVERIFY( !QgsGeometryUtils::pointContinuesArc( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 1.01, -1 ), 0.000000001, 0.05 ) );
}
QGSTEST_MAIN( TestQgsGeometryUtils )
#include "testqgsgeometryutils.moc"

View File

@ -5183,6 +5183,32 @@ class TestQgsGeometry(unittest.TestCase):
self.assertAlmostEqual(o, exp, 5,
"mismatch for {} to {}, expected:\n{}\nGot:\n{}\n".format(t[0], t[1], exp, o))
def testConvertToCurves(self):
tests = [
["LINESTRING Z (3 3 3,2.4142135623731 1.58578643762691 3,1 1 3,-0.414213562373092 1.5857864376269 3,-1 2.99999999999999 3,-0.414213562373101 4.41421356237309 3,0.999999999999991 5 3,2.41421356237309 4.4142135623731 3,3 3 3)",
"CompoundCurveZ (CircularStringZ (3 3 3, -1 2.99999999999998979 3, 3 3 3))", 0.00000001, 0.0000001],
["LINESTRING(0 0,10 0,10 10,0 10,0 0)", "CompoundCurve((0 0,10 0,10 10,0 10,0 0))", 0.00000001, 0.00000001],
["LINESTRING(0 0,10 0,10 10,0 10)", "CompoundCurve((0 0,10 0,10 10,0 10))", 0.00000001, 0.00000001],
["LINESTRING(10 10,0 10,0 0,10 0)", "CompoundCurve((10 10,0 10,0 0,10 0))", 0.0000001, 0.00000001],
["LINESTRING(0 0, 1 1)", "CompoundCurve((0 0, 1 1))", 0.00000001, 0.00000001],
["GEOMETRYCOLLECTION(LINESTRING(10 10,10 11),LINESTRING(10 11,11 11),LINESTRING(11 11,10 10))",
"MultiCurve (CompoundCurve ((10 10, 10 11)),CompoundCurve ((10 11, 11 11)),CompoundCurve ((11 11, 10 10)))", 0.000001, 0.000001],
["GEOMETRYCOLLECTION(LINESTRING(4 4,4 8),CIRCULARSTRING(4 8,6 10,8 8),LINESTRING(8 8,8 4))",
"MultiCurve (CompoundCurve ((4 4, 4 8)),CompoundCurve (CircularString (4 8, 6 10, 8 8)),CompoundCurve ((8 8, 8 4)))", 0.0000001, 0.0000001],
["LINESTRING(-13151357.927248 3913656.64539871,-13151419.0845266 3913664.12016378,-13151441.323537 3913666.61175286,-13151456.8908442 3913666.61175286,-13151476.9059536 3913666.61175286,-13151496.921063 3913666.61175287,-13151521.3839744 3913666.61175287,-13151591.4368571 3913665.36595828)",
"CompoundCurve ((-13151357.92724799923598766 3913656.64539870992302895, -13151419.08452660031616688 3913664.12016378017142415, -13151441.32353699952363968 3913666.61175285978242755, -13151456.8908441998064518 3913666.61175285978242755, -13151476.90595359914004803 3913666.61175285978242755, -13151496.92106300033628941 3913666.61175287002697587, -13151521.38397439941763878 3913666.61175287002697587, -13151591.43685710057616234 3913665.36595827993005514))", 0.000001, 0.0000001],
["Point( 1 2 )", "Point( 1 2 )", 0.00001, 0.00001],
["MultiPoint( 1 2, 3 4 )", "MultiPoint( (1 2 ), (3 4 ))", 0.00001, 0.00001]
]
for t in tests:
g1 = QgsGeometry.fromWkt(t[0])
distance_tolerance = t[2]
angle_tolerance = t[3]
o = g1.convertToCurves(distance_tolerance, angle_tolerance)
self.assertTrue(compareWkt(o.asWkt(), t[1], 0.00001),
"clipped: mismatch Expected:\n{}\nGot:\n{}\n".format(t[1], o.asWkt()))
def testBoundingBoxIntersects(self):
tests = [
["LINESTRING (0 0, 100 100)", "LINESTRING (90 0, 100 0)", True],