diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in index 24f71d05d8f8..f6effdc2cc8b 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in @@ -489,6 +489,8 @@ Calculates Cartesian azimuth between points (``x1``, ``y1``) and (``x2``, ``y2`` .. versionadded:: 3.34 %End + + }; /************************************************************************ * This file has been generated automatically from * diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 93eb673d1c82..c479998c0306 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -2555,6 +2555,7 @@ Compare two doubles, treating nan values as equal .. versionadded:: 3.20 %End + bool qgsDoubleNear( double a, double b, double epsilon = 4 * DBL_EPSILON ); %Docstring Compare two doubles (but allow some difference) diff --git a/python/PyQt6/core/auto_generated/qgspointxy.sip.in b/python/PyQt6/core/auto_generated/qgspointxy.sip.in index a442c9d08b7b..df2fb0a6f170 100644 --- a/python/PyQt6/core/auto_generated/qgspointxy.sip.in +++ b/python/PyQt6/core/auto_generated/qgspointxy.sip.in @@ -221,7 +221,23 @@ Compares this point with another point with a fuzzy tolerance :return: ``True`` if points are equal within specified tolerance +.. seealso:: :py:func:`distanceCompare` + .. versionadded:: 2.9 +%End + + bool distanceCompare( const QgsPointXY &other, double epsilon = 4 * DBL_EPSILON ) const /HoldGIL/; +%Docstring +Compares this point with another point with a fuzzy tolerance using distance comparison + +:param other: point to compare with +:param epsilon: maximum difference for coordinates between the points + +:return: ``True`` if points are equal within specified tolerance + +.. seealso:: :py:func:`compare` + +.. versionadded:: 3.36 %End bool operator==( const QgsPointXY &other ) /HoldGIL/; diff --git a/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in b/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in index 24f71d05d8f8..f6effdc2cc8b 100644 --- a/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in @@ -489,6 +489,8 @@ Calculates Cartesian azimuth between points (``x1``, ``y1``) and (``x2``, ``y2`` .. versionadded:: 3.34 %End + + }; /************************************************************************ * This file has been generated automatically from * diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 0a208cdb697d..e28de3c03201 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -2555,6 +2555,7 @@ Compare two doubles, treating nan values as equal .. versionadded:: 3.20 %End + bool qgsDoubleNear( double a, double b, double epsilon = 4 * DBL_EPSILON ); %Docstring Compare two doubles (but allow some difference) diff --git a/python/core/auto_generated/qgspointxy.sip.in b/python/core/auto_generated/qgspointxy.sip.in index 45a7212f8e6d..75140e1d8290 100644 --- a/python/core/auto_generated/qgspointxy.sip.in +++ b/python/core/auto_generated/qgspointxy.sip.in @@ -221,7 +221,23 @@ Compares this point with another point with a fuzzy tolerance :return: ``True`` if points are equal within specified tolerance +.. seealso:: :py:func:`distanceCompare` + .. versionadded:: 2.9 +%End + + bool distanceCompare( const QgsPointXY &other, double epsilon = 4 * DBL_EPSILON ) const /HoldGIL/; +%Docstring +Compares this point with another point with a fuzzy tolerance using distance comparison + +:param other: point to compare with +:param epsilon: maximum difference for coordinates between the points + +:return: ``True`` if points are equal within specified tolerance + +.. seealso:: :py:func:`compare` + +.. versionadded:: 3.36 %End bool operator==( const QgsPointXY &other ) /HoldGIL/; diff --git a/src/analysis/vector/geometry_checker/qgsgeometrycheckerutils.h b/src/analysis/vector/geometry_checker/qgsgeometrycheckerutils.h index 4c41cba5a159..3a03a1af1cfd 100644 --- a/src/analysis/vector/geometry_checker/qgsgeometrycheckerutils.h +++ b/src/analysis/vector/geometry_checker/qgsgeometrycheckerutils.h @@ -251,19 +251,6 @@ class ANALYSIS_EXPORT QgsGeometryCheckerUtils static double sharedEdgeLength( const QgsAbstractGeometry *geom1, const QgsAbstractGeometry *geom2, double tol ); - /** - * \brief Determine whether two points are equal up to the specified tolerance - * \param p1 The first point - * \param p2 The second point - * \param tol The tolerance - * \returns Whether the points are equal - */ - static inline bool pointsFuzzyEqual( const QgsPointXY &p1, const QgsPointXY &p2, double tol ) - { - double dx = p1.x() - p2.x(), dy = p1.y() - p2.y(); - return ( dx * dx + dy * dy ) < tol * tol; - } - static inline bool canDeleteVertex( const QgsAbstractGeometry *geom, int iPart, int iRing ) { const int nVerts = geom->vertexCount( iPart, iRing ); diff --git a/src/analysis/vector/geometry_checker/qgsgeometrygapcheck.cpp b/src/analysis/vector/geometry_checker/qgsgeometrygapcheck.cpp index 9a1f9a6b2091..3ae73119960d 100644 --- a/src/analysis/vector/geometry_checker/qgsgeometrygapcheck.cpp +++ b/src/analysis/vector/geometry_checker/qgsgeometrygapcheck.cpp @@ -504,7 +504,7 @@ QgsRectangle QgsGeometryGapCheckError::contextBoundingBox() const bool QgsGeometryGapCheckError::isEqual( QgsGeometryCheckError *other ) const { QgsGeometryGapCheckError *err = dynamic_cast( other ); - return err && QgsGeometryCheckerUtils::pointsFuzzyEqual( err->location(), location(), mCheck->context()->reducedTolerance ) && err->neighbors() == neighbors(); + return err && err->location().distanceCompare( location(), mCheck->context()->reducedTolerance ) && err->neighbors() == neighbors(); } bool QgsGeometryGapCheckError::closeMatch( QgsGeometryCheckError *other ) const diff --git a/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.cpp b/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.cpp index eeaa7594a3e6..b5504f0c07e0 100644 --- a/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.cpp +++ b/src/analysis/vector/geometry_checker/qgsgeometryoverlapcheck.cpp @@ -134,7 +134,7 @@ void QgsGeometryOverlapCheck::fixError( const QMap &f { QgsAbstractGeometry *part = QgsGeometryCheckerUtils::getGeomPart( interGeom.get(), iPart ); if ( std::fabs( part->area() - overlapError->value().toDouble() ) < mContext->reducedTolerance && - QgsGeometryCheckerUtils::pointsFuzzyEqual( part->centroid(), overlapError->location(), mContext->reducedTolerance ) ) + QgsGeometryUtilsBase::fuzzyDistanceEqual( mContext->reducedTolerance, part->centroid().x(), part->centroid().y(), overlapError->location().x(), overlapError->location().y() ) ) // TODO: add fuzzyDistanceEqual in QgsAbstractGeometry classes { interPart = part; break; @@ -278,7 +278,7 @@ bool QgsGeometryOverlapCheckError::isEqual( QgsGeometryCheckError *other ) const other->layerId() == layerId() && other->featureId() == featureId() && err->overlappedFeature() == overlappedFeature() && - QgsGeometryCheckerUtils::pointsFuzzyEqual( location(), other->location(), mCheck->context()->reducedTolerance ) && + location().distanceCompare( other->location(), mCheck->context()->reducedTolerance ) && std::fabs( value().toDouble() - other->value().toDouble() ) < mCheck->context()->reducedTolerance; } diff --git a/src/core/geometry/qgsgeometryutils_base.h b/src/core/geometry/qgsgeometryutils_base.h index ebd58106d43f..328fabccf4d8 100644 --- a/src/core/geometry/qgsgeometryutils_base.h +++ b/src/core/geometry/qgsgeometryutils_base.h @@ -19,6 +19,7 @@ email : loic dot bartoletti at oslandia dot com #include "qgis_sip.h" #include "qgsvector3d.h" #include "qgsvector.h" +#include /** * \ingroup core @@ -464,4 +465,77 @@ class CORE_EXPORT QgsGeometryUtilsBase * \since QGIS 3.34 */ static double azimuth( double x1, double y1, double x2, double y2 ) SIP_HOLDGIL; + +#ifndef SIP_RUN + + /** + * Performs fuzzy comparison between pairs of values within a specified epsilon. + * + * This function compares a variable number of pairs of values to check if their differences + * fall within a specified epsilon range using qgsNumberNear. It returns true if all the differences + * are within the given epsilon range; otherwise, it returns false. + * + * \tparam T Floating-point type (double or float) for the values to be compared. + * \tparam Args Type of arguments for the values to be compared. + * \param epsilon The range within which the differences are checked. + * \param args Variadic list of values to be compared in pairs. + * The number of arguments must be greater than 0 and even. + * It must follow the pattern: x1, y1, x2, y2, or x1, y1, z1, x2, y2, z2, ... + * \return true if all the differences between pairs of values are within epsilon, false otherwise. + * + * \see fuzzyDistanceEqual + * + * \since QGIS 3.36 + */ + template + static bool fuzzyEqual( T epsilon, const Args &... args ) noexcept + { + static_assert( ( sizeof...( args ) % 2 == 0 || sizeof...( args ) != 0 ), "The number of arguments must be greater than 0 and even" ); + constexpr size_t numArgs = sizeof...( args ); + bool result = true; + T values[] = {static_cast( args )...}; + + for ( size_t i = 0; i < numArgs / 2; ++i ) + { + result = result && qgsNumberNear( values[i], values[i + numArgs / 2], epsilon ); + } + + return result; + } + + /** + * Compare equality between multiple pairs of values with a specified epsilon. + * + * \tparam T Floating-point type (double or float) for the values to be compared. + * \tparam Args Type of arguments for the values to be compared. + * \param epsilon The range within which the differences are checked. + * \param args Variadic list of values to be compared in pairs. + * The number of arguments must be greater than or equal to 4. + * It must follow the pattern: x1, y1, x2, y2, or x1, y1, z1, x2, y2, z2, ... + * \return true if the squares of differences between pairs of values sum up to less than epsilon squared, false otherwise. + * + * \see fuzzyEqual + * + * \since QGIS 3.36 + */ + template + static bool fuzzyDistanceEqual( T epsilon, const Args &... args ) noexcept + { + static_assert( ( sizeof...( args ) % 2 == 0 || sizeof...( args ) >= 4 ), "The number of arguments must be greater than 4 and even" ); + constexpr size_t numArgs = sizeof...( args ); + const T squaredEpsilon = epsilon * epsilon; + T sum = 0; + + T values[] = {static_cast( args )...}; + + for ( size_t i = 0; i < numArgs / 2; ++i ) + { + const T diff = values[i] - values[i + numArgs / 2]; + sum += diff * diff; + } + + return sum < squaredEpsilon; + } +#endif + }; diff --git a/src/core/qgis.h b/src/core/qgis.h index 5508941ba54b..5eb8c04bbb1b 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -4419,22 +4419,38 @@ inline bool qgsNanCompatibleEquals( double a, double b ) return a == b; } +#ifndef SIP_RUN + /** - * Compare two doubles (but allow some difference) - * \param a first double - * \param b second double - * \param epsilon maximum difference allowable between doubles + * Compare two numbers of type T (but allow some difference) + * \param a first number + * \param b second number + * \param epsilon maximum difference allowable between numbers + * \since 3.36 */ -inline bool qgsDoubleNear( double a, double b, double epsilon = 4 * std::numeric_limits::epsilon() ) +template +inline bool qgsNumberNear( T a, T b, T epsilon = std::numeric_limits::epsilon() * 4 ) { const bool aIsNan = std::isnan( a ); const bool bIsNan = std::isnan( b ); if ( aIsNan || bIsNan ) return aIsNan && bIsNan; - const double diff = a - b; + const T diff = a - b; return diff >= -epsilon && diff <= epsilon; } +#endif + +/** + * Compare two doubles (but allow some difference) + * \param a first double + * \param b second double + * \param epsilon maximum difference allowable between doubles + */ +inline bool qgsDoubleNear( double a, double b, double epsilon = 4 * std::numeric_limits::epsilon() ) +{ + return qgsNumberNear( a, b, epsilon ); +} /** * Compare two floats (but allow some difference) @@ -4444,13 +4460,7 @@ inline bool qgsDoubleNear( double a, double b, double epsilon = 4 * std::numeric */ inline bool qgsFloatNear( float a, float b, float epsilon = 4 * FLT_EPSILON ) { - const bool aIsNan = std::isnan( a ); - const bool bIsNan = std::isnan( b ); - if ( aIsNan || bIsNan ) - return aIsNan && bIsNan; - - const float diff = a - b; - return diff >= -epsilon && diff <= epsilon; + return qgsNumberNear( a, b, epsilon ); } //! Compare two doubles using specified number of significant digits diff --git a/src/core/qgspointxy.h b/src/core/qgspointxy.h index 419c504d3099..970f078af286 100644 --- a/src/core/qgspointxy.h +++ b/src/core/qgspointxy.h @@ -28,6 +28,7 @@ #include #include #include +#include class QgsPoint; @@ -254,11 +255,29 @@ class CORE_EXPORT QgsPointXY * \param other point to compare with * \param epsilon maximum difference for coordinates between the points * \returns TRUE if points are equal within specified tolerance + * + * \see distanceCompare + * * \since QGIS 2.9 */ bool compare( const QgsPointXY &other, double epsilon = 4 * std::numeric_limits::epsilon() ) const SIP_HOLDGIL { - return ( qgsDoubleNear( mX, other.x(), epsilon ) && qgsDoubleNear( mY, other.y(), epsilon ) ); + return QgsGeometryUtilsBase::fuzzyEqual( epsilon, mX, mY, other.x(), other.y() ); + } + + /** + * Compares this point with another point with a fuzzy tolerance using distance comparison + * \param other point to compare with + * \param epsilon maximum difference for coordinates between the points + * \returns TRUE if points are equal within specified tolerance + * + * \see compare + * + * \since QGIS 3.36 + */ + bool distanceCompare( const QgsPointXY &other, double epsilon = 4 * std::numeric_limits::epsilon() ) const SIP_HOLDGIL + { + return QgsGeometryUtilsBase::fuzzyDistanceEqual( epsilon, mX, mY, other.x(), other.y() ); } //! equality operator @@ -271,11 +290,7 @@ class CORE_EXPORT QgsPointXY if ( ! isEmpty() && other.isEmpty() ) return false; - bool equal = true; - equal &= qgsDoubleNear( other.x(), mX, 1E-8 ); - equal &= qgsDoubleNear( other.y(), mY, 1E-8 ); - - return equal; + return QgsGeometryUtilsBase::fuzzyEqual( 1E-8, mX, mY, other.x(), other.y() ); } //! Inequality operator @@ -288,11 +303,7 @@ class CORE_EXPORT QgsPointXY if ( ! isEmpty() && other.isEmpty() ) return true; - bool equal = true; - equal &= qgsDoubleNear( other.x(), mX, 1E-8 ); - equal &= qgsDoubleNear( other.y(), mY, 1E-8 ); - - return !equal; + return !QgsGeometryUtilsBase::fuzzyEqual( 1E-8, mX, mY, other.x(), other.y() ); } //! Multiply x and y by the given value diff --git a/tests/src/core/geometry/CMakeLists.txt b/tests/src/core/geometry/CMakeLists.txt index 942df05f21e0..ce16cf246193 100644 --- a/tests/src/core/geometry/CMakeLists.txt +++ b/tests/src/core/geometry/CMakeLists.txt @@ -41,3 +41,4 @@ endforeach(TESTSRC) add_qgis_test(testqgsgeometry.cpp MODULE core LINKEDLIBRARIES qgis_core) add_qgis_test(testqgsgeometrycollection.cpp MODULE core LINKEDLIBRARIES qgis_core) add_qgis_test(testqgsgeometryutils.cpp MODULE core LINKEDLIBRARIES qgis_core) +add_qgis_test(testqgsgeometryutilsbase.cpp MODULE core LINKEDLIBRARIES qgis_core) diff --git a/tests/src/core/geometry/testqgsgeometryutilsbase.cpp b/tests/src/core/geometry/testqgsgeometryutilsbase.cpp new file mode 100644 index 000000000000..036b4864bec2 --- /dev/null +++ b/tests/src/core/geometry/testqgsgeometryutilsbase.cpp @@ -0,0 +1,59 @@ +/*************************************************************************** + testqgsgeometryutilsbase.cpp + -------------------------------------- + Date : 2023-12-15 + Copyright : (C) 2023 by Loïc Bartoletti + Email : loic dot bartoletti at 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 "qgstest.h" +#include +#include "qgsgeometryutils_base.h" + +class TestQgsGeometryUtilsBase: public QObject +{ + Q_OBJECT + + private slots: + void testFuzzyEqual(); + void testFuzzyDistanceEqual(); +}; +void TestQgsGeometryUtilsBase::testFuzzyEqual() +{ + QVERIFY( QgsGeometryUtilsBase::fuzzyEqual( 0.1, 1.0, 2.0, 1.0, 2.0 ) ); + + QVERIFY( QgsGeometryUtilsBase::fuzzyEqual( 1.0, 1.0, 2.0, 1.5, 2.5 ) ); + + QVERIFY( QgsGeometryUtilsBase::fuzzyEqual( 0.01, 1.0, 2.0, 1.001, 2.001 ) ); + + QVERIFY( !QgsGeometryUtilsBase::fuzzyEqual( 0.1, 1.0, 2.0, 1.5, 2.0 ) ); + + QVERIFY( !QgsGeometryUtilsBase::fuzzyEqual( 0.4, 1.0, 2.0, 1.5, 2.5 ) ); + + QVERIFY( !QgsGeometryUtilsBase::fuzzyEqual( 0.001, 1.0, 2.0, 1.1, 2.1 ) ); +} + +void TestQgsGeometryUtilsBase::testFuzzyDistanceEqual() +{ + QVERIFY( QgsGeometryUtilsBase::fuzzyDistanceEqual( 0.1, 1.0, 2.0, 1.0, 2.0 ) ); + + QVERIFY( QgsGeometryUtilsBase::fuzzyDistanceEqual( 2.0, 1.0, 2.0, 1.5, 2.1 ) ); + + QVERIFY( QgsGeometryUtilsBase::fuzzyDistanceEqual( 0.01, 1.0, 2.0, 1.001, 2.001 ) ); + + QVERIFY( !QgsGeometryUtilsBase::fuzzyDistanceEqual( 0.1, 1.0, 2.0, 1.5, 2.0 ) ); + + QVERIFY( !QgsGeometryUtilsBase::fuzzyDistanceEqual( 0.2, 1.0, 2.0, 1.5, 2.5 ) ); + + QVERIFY( !QgsGeometryUtilsBase::fuzzyDistanceEqual( 0.001, 1.0, 2.0, 1.1, 2.1 ) ); +} + +QGSTEST_MAIN( TestQgsGeometryUtilsBase ) +#include "testqgsgeometryutilsbase.moc"