Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add roundness method to curve polygon (#45154)
* add roundness function

* add roundness function tests

* add roundness function help

* add roundness expression

* add roundness expression tests

* add roundness expression help

* add roundness processing

* add roundness processing tests

* add roundness processing help

* fix import and typo

* Fix typo

Co-authored-by: Harrissou Sant-anna <delazj@gmail.com>

* Add tag

Co-authored-by: Harrissou Sant-anna <delazj@gmail.com>

* Fix typo

Co-authored-by: Harrissou Sant-anna <delazj@gmail.com>

* Modify the description

Co-authored-by: Harrissou Sant-anna <delazj@gmail.com>

* Replace equality condition by qgsDoubleNear

Co-authored-by: Loïc Bartoletti <lbartoletti@users.noreply.github.com>

* Change types for literals

Co-authored-by: Loïc Bartoletti <lbartoletti@users.noreply.github.com>

* add since version

Co-authored-by: Loïc Bartoletti <lbartoletti@users.noreply.github.com>

* add a const

* add the \since adding in the sip file

* Fix typo

Co-authored-by: Loïc Bartoletti <lbartoletti@users.noreply.github.com>

* Fix typo

Co-authored-by: Loïc Bartoletti <lbartoletti@users.noreply.github.com>

* Remove SIP factory annotation

Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>

* Remove double calculation of permimeter

Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>

* Always add an attribute

Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>

* Use QGSCOMPARENEAR in tests

Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>

* Shorten the short description

Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>

* Make a more complete help string

Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>

* Correct the description

Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>

* reformat bad code styles

* sipify

Co-authored-by: Harrissou Sant-anna <delazj@gmail.com>
Co-authored-by: Loïc Bartoletti <lbartoletti@users.noreply.github.com>
Co-authored-by: Nyall Dawson <nyall.dawson@gmail.com>
  • Loading branch information
4 people committed Nov 4, 2021
1 parent 209fd38 commit 97b288d
Show file tree
Hide file tree
Showing 12 changed files with 304 additions and 0 deletions.
8 changes: 8 additions & 0 deletions python/core/auto_generated/geometry/qgscurvepolygon.sip.in
Expand Up @@ -75,6 +75,14 @@ Curve polygon geometry type
virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;


double roundness() const;
%Docstring
Returns the roundness of the curve polygon.
The returned value is between 0 and 1.

.. versionadded:: 3.24
%End


int numInteriorRings() const /HoldGIL/;
%Docstring
Expand Down
12 changes: 12 additions & 0 deletions resources/function_help/json/roundness
@@ -0,0 +1,12 @@
{
"name": "roundness",
"type": "function",
"groups": ["GeometryGroup"],
"description": "Calculates how close a polygon shape is to a circle. The function returns 1 when the polygon shape is a perfect circle and 0 when it is completely flat.",
"arguments": [
{"arg":"geometry","description":"a polygon"}],
"examples": [
{ "expression":"round(roundness(geom_from_wkt('POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0))')), 3)", "returns":"0.785"},
{ "expression":"round(roundness(geom_from_wkt('POLYGON(( 0 0, 0 0.1, 1 0.1, 1 0, 0 0))')), 3)", "returns":"0.260"}
]
}
1 change: 1 addition & 0 deletions src/analysis/CMakeLists.txt
Expand Up @@ -173,6 +173,7 @@ set(QGIS_ANALYSIS_SRCS
processing/qgsalgorithmrescaleraster.cpp
processing/qgsalgorithmreverselinedirection.cpp
processing/qgsalgorithmrotate.cpp
processing/qgsalgorithmroundness.cpp
processing/qgsalgorithmroundrastervalues.cpp
processing/qgsalgorithmruggedness.cpp
processing/qgsalgorithmsavefeatures.cpp
Expand Down
113 changes: 113 additions & 0 deletions src/analysis/processing/qgsalgorithmroundness.cpp
@@ -0,0 +1,113 @@
/***************************************************************************
qgsalgorithmroundness.cpp
---------------------
begin : September 2021
copyright : (C) 2021 by Antoine Facchini
email : antoine dot facchini @oslandia 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 "qgsalgorithmroundness.h"
#include "qgscurvepolygon.h"

///@cond PRIVATE

QString QgsRoundnessAlgorithm::name() const
{
return QStringLiteral( "roundness" );
}

QString QgsRoundnessAlgorithm::displayName() const
{
return QObject::tr( "Roundness" );
}

QStringList QgsRoundnessAlgorithm::tags() const
{
return QObject::tr( "roundness,circle" ).split( ',' );
}

QString QgsRoundnessAlgorithm::group() const
{
return QObject::tr( "Vector geometry" );
}

QString QgsRoundnessAlgorithm::groupId() const
{
return QStringLiteral( "vectorgeometry" );
}

QString QgsRoundnessAlgorithm::outputName() const
{
return QObject::tr( "Roundness" );
}

QString QgsRoundnessAlgorithm::shortHelpString() const
{
return QObject::tr( "Calculates the roundness of each feature and stores it as a new field. The input vector layer must contain polygons.\n\n"
"The roundness of a polygon is defined as 4π × polygon area / perimeter². The roundness value varies between 0 and 1. A perfect circle has a roundness of 1, while a completely flat polygon has a roundness of 0." );
}

QString QgsRoundnessAlgorithm::shortDescription() const
{
return QObject::tr( "Calculates the roundness of polygon features." );
}

QgsRoundnessAlgorithm *QgsRoundnessAlgorithm::createInstance() const
{
return new QgsRoundnessAlgorithm();
}

QList<int> QgsRoundnessAlgorithm::inputLayerTypes() const
{
return QList<int>() << QgsProcessing::TypeVectorPolygon;
}

QgsProcessing::SourceType QgsRoundnessAlgorithm::outputLayerType() const
{
return QgsProcessing::TypeVectorPolygon;
}

QgsFields QgsRoundnessAlgorithm::outputFields( const QgsFields &inputFields ) const
{
QgsFields outputFields = inputFields;
outputFields.append( QgsField( QStringLiteral( "roundness" ), QVariant::Double ) );
return outputFields;
}

QgsFeatureList QgsRoundnessAlgorithm::processFeature( const QgsFeature &feature, QgsProcessingContext &, QgsProcessingFeedback * )
{
QgsFeature f = feature;
QgsAttributes attributes = f.attributes();
if ( f.hasGeometry() )
{
QgsGeometry geom = f.geometry();
if ( const QgsCurvePolygon *poly = qgsgeometry_cast< const QgsCurvePolygon * >( geom.constGet()->simplifiedTypeRef() ) )
{
double roundness = poly->roundness();
attributes << QVariant( roundness );
}
else
{
attributes << QVariant();
}
}
else
{
attributes << QVariant();
}
f.setAttributes( attributes );
return QgsFeatureList() << f;
}

///@endcond


60 changes: 60 additions & 0 deletions src/analysis/processing/qgsalgorithmroundness.h
@@ -0,0 +1,60 @@
/***************************************************************************
qgsalgorithmroundness.h
---------------------
begin : September 2021
copyright : (C) 2021 by Antoine Facchini
email : antoine dot facchini @oslandia 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 QGSALGORITHMROUNDNESS_H
#define QGSALGORITHMROUNDNESS_H

#define SIP_NO_FILE

#include "qgis_sip.h"
#include "qgsprocessingalgorithm.h"

///@cond PRIVATE

/**
* Native roundness algorithm.
*/
class QgsRoundnessAlgorithm : public QgsProcessingFeatureBasedAlgorithm
{

public:

QgsRoundnessAlgorithm() = 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;
QString shortDescription() const override;
QgsRoundnessAlgorithm *createInstance() const override SIP_FACTORY;
QList<int> inputLayerTypes() const override;

protected:
QString outputName() const override;
QgsProcessing::SourceType outputLayerType() const override;
QgsFields outputFields( const QgsFields &inputFields ) const override;

QgsFeatureList processFeature( const QgsFeature &feature, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override;
};


///@endcond PRIVATE

#endif // QGSALGORITHMROUNDNESS_H


2 changes: 2 additions & 0 deletions src/analysis/processing/qgsnativealgorithms.cpp
Expand Up @@ -163,6 +163,7 @@
#include "qgsalgorithmrescaleraster.h"
#include "qgsalgorithmreverselinedirection.h"
#include "qgsalgorithmrotate.h"
#include "qgsalgorithmroundness.h"
#include "qgsalgorithmroundrastervalues.h"
#include "qgsalgorithmruggedness.h"
#include "qgsalgorithmsavefeatures.h"
Expand Down Expand Up @@ -442,6 +443,7 @@ void QgsNativeAlgorithms::loadAlgorithms()
addAlgorithm( new QgsRetainTableFieldsAlgorithm() );
addAlgorithm( new QgsReverseLineDirectionAlgorithm() );
addAlgorithm( new QgsRotateFeaturesAlgorithm() );
addAlgorithm( new QgsRoundnessAlgorithm() );
addAlgorithm( new QgsRoundRasterValuesAlgorithm() );
addAlgorithm( new QgsRuggednessAlgorithm() );
addAlgorithm( new QgsSaveFeaturesAlgorithm() );
Expand Down
18 changes: 18 additions & 0 deletions src/core/expression/qgsexpressionfunction.cpp
Expand Up @@ -3861,6 +3861,20 @@ static QVariant fcnStraightDistance2d( const QVariantList &values, const QgsExpr
return QVariant( curve->straightDistance2d() );
}

static QVariant fcnRoundness( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
{
QgsGeometry geom = QgsExpressionUtils::getGeometry( values.at( 0 ), parent );
const QgsCurvePolygon *poly = geom.constGet() ? qgsgeometry_cast< const QgsCurvePolygon * >( geom.constGet()->simplifiedTypeRef() ) : nullptr;

if ( !poly )
{
parent->setEvalErrorString( QObject::tr( "Function `roundness` requires a polygon geometry or a multi polygon geometry with a single part." ) );
return QVariant();
}

return QVariant( poly->roundness() );
}



static QVariant fcnFlipCoordinates( const QVariantList &values, const QgsExpressionContext *, QgsExpression *parent, const QgsExpressionNodeFunction * )
Expand Down Expand Up @@ -7134,6 +7148,10 @@ const QList<QgsExpressionFunction *> &QgsExpression::Functions()

functions << new QgsStaticExpressionFunction( QStringLiteral( "perimeter" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ), fcnPerimeter, QStringLiteral( "GeometryGroup" ) );

functions << new QgsStaticExpressionFunction( QStringLiteral( "roundness" ),
QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "geometry" ) ),
fcnRoundness, QStringLiteral( "GeometryGroup" ) );

QgsStaticExpressionFunction *xFunc = new QgsStaticExpressionFunction( QStringLiteral( "$x" ), 0, fcnX, QStringLiteral( "GeometryGroup" ), QString(), true );
xFunc->setIsStatic( false );
functions << xFunc;
Expand Down
9 changes: 9 additions & 0 deletions src/core/geometry/qgscurvepolygon.cpp
Expand Up @@ -527,6 +527,15 @@ double QgsCurvePolygon::perimeter() const
return perimeter;
}

double QgsCurvePolygon::roundness() const
{
const double p = perimeter();
if ( qgsDoubleNear( p, 0.0 ) )
return 0.0;

return 4.0 * M_PI * area() / pow( p, 2.0 );
}

QgsPolygon *QgsCurvePolygon::surfaceToPolygon() const
{
std::unique_ptr< QgsPolygon > polygon( new QgsPolygon() );
Expand Down
7 changes: 7 additions & 0 deletions src/core/geometry/qgscurvepolygon.h
Expand Up @@ -69,6 +69,13 @@ class CORE_EXPORT QgsCurvePolygon: public QgsSurface
bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits<double>::epsilon(), bool useZValues = false ) override;
bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL;

/**
* Returns the roundness of the curve polygon.
* The returned value is between 0 and 1.
* \since QGIS 3.24
*/
double roundness() const;

//curve polygon interface

/**
Expand Down
41 changes: 41 additions & 0 deletions tests/src/analysis/testqgsprocessingalgs.cpp
Expand Up @@ -103,6 +103,9 @@ class TestQgsProcessingAlgs: public QObject
void polygonsToLines_data();
void polygonsToLines();

void roundness_data();
void roundness();

void createConstantRaster_data();
void createConstantRaster();

Expand Down Expand Up @@ -1438,6 +1441,44 @@ void TestQgsProcessingAlgs::polygonsToLines()
QVERIFY2( result.geometry().equals( expectedGeometry ), QStringLiteral( "Result: %1, Expected: %2" ).arg( result.geometry().asWkt(), expectedGeometry.asWkt() ).toUtf8().constData() );
}

void TestQgsProcessingAlgs::roundness_data()
{
QTest::addColumn<QgsGeometry>( "sourceGeometry" );
QTest::addColumn<double>( "expectedAttribute" );

QTest::newRow( "Polygon" )
<< QgsGeometry::fromWkt( "POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0 ))" )
<< 0.785;

QTest::newRow( "Thin polygon" )
<< QgsGeometry::fromWkt( "POLYGON(( 0 0, 0.5 0, 1 0, 0.6 0, 0 0 ))" )
<< 0.0;

QTest::newRow( "Circle polygon" )
<< QgsGeometry::fromWkt( "CurvePolygon (CompoundCurve (CircularString (0 0, 0 1, 1 1, 1 0, 0 0)))" )
<< 1.0;

QTest::newRow( "Polygon with hole" )
<< QgsGeometry::fromWkt( "POLYGON(( 0 0, 0 3, 3 3, 3 0, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))" )
<< 0.393;
}

void TestQgsProcessingAlgs::roundness()
{
QFETCH( QgsGeometry, sourceGeometry );
QFETCH( double, expectedAttribute );

const std::unique_ptr< QgsProcessingFeatureBasedAlgorithm > alg( featureBasedAlg( "native:roundness" ) );

QgsFeature feature;
feature.setGeometry( sourceGeometry );

const QgsFeature result = runForFeature( alg, feature, QStringLiteral( "Polygon" ) );

const double roundnessResult = result.attribute( QStringLiteral( "roundness" ) ).toDouble();
QGSCOMPARENEAR( roundnessResult, expectedAttribute, 0.001 );
}

Q_DECLARE_METATYPE( Qgis::DataType )
void TestQgsProcessingAlgs::createConstantRaster_data()
{
Expand Down
25 changes: 25 additions & 0 deletions tests/src/core/geometry/testqgscurvepolygon.cpp
Expand Up @@ -55,6 +55,7 @@ class TestQgsCurvePolygon: public QObject
void testClosestSegment();
void testBoundary();
void testBoundingBox();
void testRoundness();
void testDropZValue();
void testDropMValue();
void testToPolygon();
Expand Down Expand Up @@ -1253,6 +1254,30 @@ void TestQgsCurvePolygon::testBoundingBox()
QGSCOMPARENEAR( bBox.yMaximum(), 18, 0.001 );
}

void TestQgsCurvePolygon::testRoundness()
{
QgsCurvePolygon poly;

//empty
QCOMPARE( poly.roundness(), 0 );

QgsCircularString ext;
ext.setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 0, 1 )
<< QgsPoint( 1, 1 ) << QgsPoint( 1, 0 ) << QgsPoint( 0, 0 ) );
poly.setExteriorRing( ext.clone() );

QCOMPARE( poly.roundness(), 1.0 );

//with Z
QgsLineString extLine;
extLine.setPoints( QgsPointSequence() << QgsPoint( 0, 0, 5 )
<< QgsPoint( 0, 0.01, 4 ) << QgsPoint( 1, 0.01, 2 )
<< QgsPoint( 1, 0, 10 ) << QgsPoint( 0, 0, 5 ) );
poly.setExteriorRing( extLine.clone() );

QGSCOMPARENEAR( poly.roundness(), 0.031, 0.001 );
}

void TestQgsCurvePolygon::testDropZValue()
{
QgsCurvePolygon poly;
Expand Down
8 changes: 8 additions & 0 deletions tests/src/core/testqgsexpression.cpp
Expand Up @@ -1457,6 +1457,14 @@ class TestQgsExpression: public QObject
QTest::newRow( "straight_distance_2d linestring" ) << "round(straight_distance_2d(geom_from_wkt('LINESTRING(1 4, 3 5, 5 0)')), 3)" << false << QVariant( 5.657 );
QTest::newRow( "straight_distance_2d closed linestring" ) << "straight_distance_2d(geom_from_wkt('LINESTRING(2 2, 3 6, 2 2)'))" << false << QVariant( 0.0 );
QTest::newRow( "straight_distance_2d circularstring" ) << "round(straight_distance_2d(geom_from_wkt('CircularString (20 30, 50 30, 10 50)')), 3)" << false << QVariant( 22.361 );
QTest::newRow( "roundness not geom" ) << "roundness('r')" << true << QVariant();
QTest::newRow( "roundness null" ) << "roundness(NULL)" << false << QVariant();
QTest::newRow( "roundness not polygon" ) << "roundness(geom_from_wkt('POINT(1 2)'))" << true << QVariant();
QTest::newRow( "roundness polygon" ) << "round(roundness(geom_from_wkt('POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0))')), 3)" << false << QVariant( 0.785 );
QTest::newRow( "roundness single part multi polygon" ) << "round(roundness(geom_from_wkt('MULTIPOLYGON (((0 0, 0 1, 1 1, 1 0, 0 0)))')), 3)" << false << QVariant( 0.785 );
QTest::newRow( "roundness multi polygon" ) << "round(roundness(geom_from_wkt('MULTIPOLYGON( ((0 0, 0 1, 1 1, 1 0, 0 0)), ((5 2, 4 9, 5 9, 6 5, 5 2)) )')))" << true << QVariant();
QTest::newRow( "roundness thin polygon" ) << "roundness(geom_from_wkt('POLYGON(( 0 0, 0.5 0, 1 0, 0.6 0, 0 0))'))" << false << QVariant( 0.0 );
QTest::newRow( "roundness circle polygon" ) << "roundness(geom_from_wkt('CurvePolygon (CompoundCurve (CircularString (0 0, 0 1, 1 1, 1 0, 0 0)))'))" << false << QVariant( 1.0 );

// string functions
QTest::newRow( "format_number" ) << "format_number(1999.567,2)" << false << QVariant( "1,999.57" );
Expand Down

0 comments on commit 97b288d

Please sign in to comment.