Skip to content

Commit

Permalink
Add fuzzy equal methods
Browse files Browse the repository at this point in the history
Add fuzzy equal methods
  • Loading branch information
lbartoletti authored Dec 19, 2023
2 parents 1d1c556 + 8d83b8d commit 6b38cb5
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
1 change: 1 addition & 0 deletions python/PyQt6/core/auto_generated/qgis.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions python/PyQt6/core/auto_generated/qgspointxy.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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/;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_generated/qgis.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions python/core/auto_generated/qgspointxy.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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/;
Expand Down
13 changes: 0 additions & 13 deletions src/analysis/vector/geometry_checker/qgsgeometrycheckerutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ QgsRectangle QgsGeometryGapCheckError::contextBoundingBox() const
bool QgsGeometryGapCheckError::isEqual( QgsGeometryCheckError *other ) const
{
QgsGeometryGapCheckError *err = dynamic_cast<QgsGeometryGapCheckError *>( 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ void QgsGeometryOverlapCheck::fixError( const QMap<QString, QgsFeaturePool *> &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;
Expand Down Expand Up @@ -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;
}

Expand Down
74 changes: 74 additions & 0 deletions src/core/geometry/qgsgeometryutils_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ email : loic dot bartoletti at oslandia dot com
#include "qgis_sip.h"
#include "qgsvector3d.h"
#include "qgsvector.h"
#include <iterator>

/**
* \ingroup core
Expand Down Expand Up @@ -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<typename T, typename... Args>
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<T>( 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<typename T, typename... Args>
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<T>( 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

};
36 changes: 23 additions & 13 deletions src/core/qgis.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>::epsilon() )
template<typename T>
inline bool qgsNumberNear( T a, T b, T epsilon = std::numeric_limits<T>::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<double>::epsilon() )
{
return qgsNumberNear<double>( a, b, epsilon );
}

/**
* Compare two floats (but allow some difference)
Expand All @@ -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<float>( a, b, epsilon );
}

//! Compare two doubles using specified number of significant digits
Expand Down
33 changes: 22 additions & 11 deletions src/core/qgspointxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <QString>
#include <QPoint>
#include <QObject>
#include <qglobal.h>

class QgsPoint;

Expand Down Expand Up @@ -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<double>::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<double>::epsilon() ) const SIP_HOLDGIL
{
return QgsGeometryUtilsBase::fuzzyDistanceEqual( epsilon, mX, mY, other.x(), other.y() );
}

//! equality operator
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/src/core/geometry/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading

0 comments on commit 6b38cb5

Please sign in to comment.