diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 0a50ab778d4..763a2ce5ce8 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -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 diff --git a/src/analysis/processing/qgsalgorithmconverttocurves.cpp b/src/analysis/processing/qgsalgorithmconverttocurves.cpp new file mode 100644 index 00000000000..0a0df4c394c --- /dev/null +++ b/src/analysis/processing/qgsalgorithmconverttocurves.cpp @@ -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 QgsConvertToCurvesAlgorithm::inputLayerTypes() const +{ + return QList() << 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 ¶meters, 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 + + diff --git a/src/analysis/processing/qgsalgorithmconverttocurves.h b/src/analysis/processing/qgsalgorithmconverttocurves.h new file mode 100644 index 00000000000..972ee69afbd --- /dev/null +++ b/src/analysis/processing/qgsalgorithmconverttocurves.h @@ -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 inputLayerTypes() const override; + void initParameters( const QVariantMap &configuration = QVariantMap() ) override; + + protected: + QString outputName() const override; + bool prepareAlgorithm( const QVariantMap ¶meters, 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 + + diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 7a1d3ab1f0d..d79c5439d52 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -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() ); diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index ffa0fb02b13..02134aa4a90 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -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 ) diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index 0c555ace867..1d1226b9671 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -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. * diff --git a/src/core/geometry/qgsgeometryutils.cpp b/src/core/geometry/qgsgeometryutils.cpp index b2812e0e0be..6b49f4e0f51 100644 --- a/src/core/geometry/qgsgeometryutils.cpp +++ b/src/core/geometry/qgsgeometryutils.cpp @@ -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; diff --git a/src/core/geometry/qgsgeometryutils.h b/src/core/geometry/qgsgeometryutils.h index 57757b99f11..6f164b4edde 100644 --- a/src/core/geometry/qgsgeometryutils.h +++ b/src/core/geometry/qgsgeometryutils.h @@ -262,7 +262,7 @@ class CORE_EXPORT QgsGeometryUtils static QVector 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. diff --git a/src/core/geometry/qgsinternalgeometryengine.cpp b/src/core/geometry/qgsinternalgeometryengine.cpp index 8b11de66028..809ffd06296 100644 --- a/src/core/geometry/qgsinternalgeometryengine.cpp +++ b/src/core/geometry/qgsinternalgeometryengine.cpp @@ -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 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 ) ); + } +} diff --git a/src/core/geometry/qgsinternalgeometryengine.h b/src/core/geometry/qgsinternalgeometryengine.h index dbbbcb17869..1e9528249aa 100644 --- a/src/core/geometry/qgsinternalgeometryengine.h +++ b/src/core/geometry/qgsinternalgeometryengine.h @@ -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; }; diff --git a/tests/src/core/testqgsgeometryutils.cpp b/tests/src/core/testqgsgeometryutils.cpp index b5be8a60051..cfe51abfd3c 100644 --- a/tests/src/core/testqgsgeometryutils.cpp +++ b/tests/src/core/testqgsgeometryutils.cpp @@ -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" diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index b6fd563645a..591ee0d72bc 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -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],