From 6f9fd7cfa9e119304020e9776714040eaf77227b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Wed, 29 Jan 2025 08:42:10 +0100 Subject: [PATCH 1/4] feat(QgsCircle): Add segment calculation methods --- python/PyQt6/core/auto_additions/qgscircle.py | 20 ++ .../auto_generated/geometry/qgscircle.sip.in | 201 +++++++++++++-- python/core/auto_additions/qgscircle.py | 20 ++ .../auto_generated/geometry/qgscircle.sip.in | 201 +++++++++++++-- src/core/geometry/qgscircle.cpp | 64 +++-- src/core/geometry/qgscircle.h | 234 ++++++++++++++++-- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgscircle.py | 132 ++++++++++ 8 files changed, 799 insertions(+), 74 deletions(-) create mode 100644 tests/src/python/test_qgscircle.py diff --git a/python/PyQt6/core/auto_additions/qgscircle.py b/python/PyQt6/core/auto_additions/qgscircle.py index 0a4bdc326d84..05b32ed8dfba 100644 --- a/python/PyQt6/core/auto_additions/qgscircle.py +++ b/python/PyQt6/core/auto_additions/qgscircle.py @@ -1,4 +1,20 @@ # The following has been generated automatically from src/core/geometry/qgscircle.h +# monkey patching scoped based enum +QgsCircle.SegmentCalculationMethod.Standard.__doc__ = "Standard sagitta-based calculation" +QgsCircle.SegmentCalculationMethod.Adaptive.__doc__ = "Adaptive calculation based on radius size" +QgsCircle.SegmentCalculationMethod.AreaError.__doc__ = "Calculation based on area error" +QgsCircle.SegmentCalculationMethod.ConstantDensity.__doc__ = "Simple calculation with constant segment density" +QgsCircle.SegmentCalculationMethod.__doc__ = """Method used to calculate the number of segments for circle approximation + +.. versionadded:: 3.44 + +* ``Standard``: Standard sagitta-based calculation +* ``Adaptive``: Adaptive calculation based on radius size +* ``AreaError``: Calculation based on area error +* ``ConstantDensity``: Simple calculation with constant segment density + +""" +# -- try: QgsCircle.from2Points = staticmethod(QgsCircle.from2Points) QgsCircle.from3Points = staticmethod(QgsCircle.from3Points) @@ -8,6 +24,10 @@ QgsCircle.from3TangentsMulti = staticmethod(QgsCircle.from3TangentsMulti) QgsCircle.fromExtent = staticmethod(QgsCircle.fromExtent) QgsCircle.minimalCircleFrom3Points = staticmethod(QgsCircle.minimalCircleFrom3Points) + QgsCircle.calculateSegments = staticmethod(QgsCircle.calculateSegments) + QgsCircle.calculateSegmentsAdaptive = staticmethod(QgsCircle.calculateSegmentsAdaptive) + QgsCircle.calculateSegmentsByAreaError = staticmethod(QgsCircle.calculateSegmentsByAreaError) + QgsCircle.calculateSegmentsByConstant = staticmethod(QgsCircle.calculateSegmentsByConstant) QgsCircle.__group__ = ['geometry'] except (NameError, AttributeError): pass diff --git a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in index 8cbcbb8a36f8..63b1d62dd548 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in @@ -14,6 +14,7 @@ + class QgsCircle : QgsEllipse { %Docstring(signature="appended") @@ -89,11 +90,7 @@ The azimuth is the angle between ``center`` and ``pt1``. %End - static QgsCircle from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, - const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, - const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, - double epsilon = 1E-8, - const QgsPoint &pos = QgsPoint() ) /HoldGIL/; + static QgsCircle from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, double epsilon = 1E-8, const QgsPoint &pos = QgsPoint() ) /HoldGIL/; %Docstring Constructs a circle by 3 tangents on the circle (aka inscribed circle of a triangle). Z and m values are dropped for the center point. @@ -125,11 +122,7 @@ Example # %End - static QVector from3TangentsMulti( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, - const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, - const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, - double epsilon = 1E-8, - const QgsPoint &pos = QgsPoint() ) /HoldGIL/; + static QVector from3TangentsMulti( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, double epsilon = 1E-8, const QgsPoint &pos = QgsPoint() ) /HoldGIL/; %Docstring Returns an array of circle constructed by 3 tangents on the circle (aka inscribed circle of a triangle). @@ -233,9 +226,7 @@ If found, the tangent points will be stored in ``pt1`` and ``pt2``. .. versionadded:: 3.2 %End - int outerTangents( const QgsCircle &other, - QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, - QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; + int outerTangents( const QgsCircle &other, QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; %Docstring Calculates the outer tangent points between this circle and an ``other`` circle. @@ -262,9 +253,7 @@ Returns the number of tangents (either 0 or 2). .. versionadded:: 3.2 %End - int innerTangents( const QgsCircle &other, - QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, - QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; + int innerTangents( const QgsCircle &other, QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; %Docstring Calculates the inner tangent points between this circle and an ``other`` circle. @@ -388,11 +377,189 @@ Coordinates are taken from quadrant North, East and South. .. seealso:: :py:func:`asGml2` %End + enum class SegmentCalculationMethod + { + Standard, + Adaptive, + AreaError, + ConstantDensity + }; + + static int calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ); +%Docstring +Calculates the number of segments needed to approximate a circle. + +:param radius: Circle radius. Must be positive; if <= 0, `minSegments` is returned. +:param parameter: Maximum tolerance allowed for the deviation between the circle and its approximation, + except for the ConstantDensity method where it is a constant. If <= 0, a default value of 0.01 is used. +:param minSegments: Minimum number of segments to use. If < 3, it is set to 3. +:param method: Calculation method to use. + +:return: Number of segments needed for the approximation. + +\pre `radius` must be strictly positive; otherwise, the function returns `minSegments`. +\pre `parameter` should be positive; if not, it defaults to 0.01. +\pre `minSegments` should be at least 3; if less, it is clamped to 3. + +.. versionadded:: 3.44 +%End + + SIP_PYOBJECT __repr__(); %MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->toString() ); + QString str + = QStringLiteral( "" ).arg( sipCpp->toString() ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); %End + + private : + + static int +%Docstring +Calculate the number of segments needed to approximate a circle within a given tolerance. + +This function uses the sagitta (geometric chord height) to determine the number of segments +required to approximate a circle such that the maximum deviation between the circle and its +polygonal approximation is less than the specified tolerance. + +Mathematical approach: +1. Using the sagitta formula: s = r(1 - cos(θ/2)) +where s is the sagitta, r is the radius, and θ is the segment angle +2. Substituting tolerance for s: +tolerance = radius(1 - cos(θ/2)) +3. Solving for θ: +tolerance/radius = 1 - cos(θ/2) +cos(θ/2) = 1 - tolerance/radius +θ/2 = arccos(1 - tolerance/radius) +θ = 2 * arccos(1 - tolerance/radius) +4. Number of segments = ceil(2π / θ) += ceil(π / arccos(1 - tolerance/radius)) + +:param radius: The radius of the circle to approximate +:param tolerance: Maximum allowed deviation between the circle and its polygonal approximation +:param minSegments: Minimum number of segments to use, regardless of the calculated value + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End + calculateSegmentsStandard( double radius, double tolerance, int minSegments ); + + static int calculateSegmentsAdaptive( double radius, double tolerance, int minSegments ); +%Docstring +Calculate the number of segments with adaptive tolerance based on radius. + +This method extends :py:func:`~QgsCircle.calculateSegments` by using an adaptive tolerance that scales +with the radius to maintain better visual quality. While :py:func:`~QgsCircle.calculateSegments` uses +a fixed tolerance, this version adjusts the tolerance based on the radius size. + +Mathematical approach: +1. Compute adaptive tolerance that varies with radius: +adaptive_tolerance = base_tolerance * sqrt(radius) / log10(radius + 1) + +- For small radii: tolerance decreases → more segments for better detail +- For large radii: tolerance increases gradually → fewer segments needed +- sqrt(radius) provides basic scaling +- log10(radius + 1) dampens the scaling for large radii + +2. Apply sagitta-based calculation: + +- Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) +- Number of segments = ceil(2π/angle) + +This adaptation ensures: + +- Small circles get more segments for better visual quality +- Large circles don't get excessive segments +- Smooth transition between different scales + +:param radius: The radius of the circle to approximate +:param tolerance: Base tolerance value that will be scaled +:param minSegments: Minimum number of segments to use + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End + + static int calculateSegmentsByAreaError( double radius, double baseTolerance, int minSegments ); +%Docstring +Calculate the number of segments based on the maximum allowed area error. + +This function computes the minimum number of segments needed to approximate +a circle with a regular polygon such that the relative area error between +the polygonal approximation and the actual circle is less than the specified tolerance. + +Mathematical derivation: +1. Area ratio between a regular n-sided polygon and a circle: + +- Circle area: Ac = πr² +- Regular polygon area: Ap = (nr²/2) * sin(2π/n) +- Ratio = Ap / Ac = (n / 2π) * sin(2π/n) + +2. For relative error E: +E = |1 - Ap / Ac| = |1 - (n / 2π) * sin(2π/n)| + +3. Using Taylor series approximation for sin(x) when x is small: +sin(x) ≈ x - x³ / 6 +With x = 2π / n: +sin(2π / n) ≈ (2π / n) - (2π / n)³ / 6 + +4. Substituting and simplifying: +E ≈ |1 - (n / 2π) * ((2π / n) - (2π / n)³ / 6)| +E ≈ |1 - (1 - (2π² / 3n²))| +E ≈ 2π² / 3n² + +5. Rearranging to find the minimum n for a given tolerance: + +- Start with the inequality: E ≤ tolerance +- Substitute the expression for E: + 2π² / 3n² ≤ tolerance +- Rearrange to isolate n²: + n² ≥ 2π² / (3 * tolerance) +- Taking the square root: + n ≥ π * sqrt(2 / (3 * tolerance)) + +:param radius: The radius of the circle to approximate +:param tolerance: Maximum acceptable area error in percentage +:param minSegments: The minimum number of segments to use + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End + + static int calculateSegmentsByConstant( double radius, double constant, int minSegments ); +%Docstring +Calculate the number of segments using a simple linear relationship with radius. + +This function implements the simplest approach to circle discretization by using +a direct linear relationship between the radius and the number of segments. +While not mathematically precise for error control, it provides a quick and +intuitive approximation that can be useful when exact error bounds aren't required. + +Mathematical approach: +1. Linear scaling: segments = constant * radius + +- Larger constant = more segments = better approximation +- Smaller constant = fewer segments = coarser approximation + +:param radius: The radius of the circle to approximate +:param constant: Multiplier that determines the density of segments +:param minSegments: The minimum number of segments to use + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End }; /************************************************************************ diff --git a/python/core/auto_additions/qgscircle.py b/python/core/auto_additions/qgscircle.py index 0a4bdc326d84..05b32ed8dfba 100644 --- a/python/core/auto_additions/qgscircle.py +++ b/python/core/auto_additions/qgscircle.py @@ -1,4 +1,20 @@ # The following has been generated automatically from src/core/geometry/qgscircle.h +# monkey patching scoped based enum +QgsCircle.SegmentCalculationMethod.Standard.__doc__ = "Standard sagitta-based calculation" +QgsCircle.SegmentCalculationMethod.Adaptive.__doc__ = "Adaptive calculation based on radius size" +QgsCircle.SegmentCalculationMethod.AreaError.__doc__ = "Calculation based on area error" +QgsCircle.SegmentCalculationMethod.ConstantDensity.__doc__ = "Simple calculation with constant segment density" +QgsCircle.SegmentCalculationMethod.__doc__ = """Method used to calculate the number of segments for circle approximation + +.. versionadded:: 3.44 + +* ``Standard``: Standard sagitta-based calculation +* ``Adaptive``: Adaptive calculation based on radius size +* ``AreaError``: Calculation based on area error +* ``ConstantDensity``: Simple calculation with constant segment density + +""" +# -- try: QgsCircle.from2Points = staticmethod(QgsCircle.from2Points) QgsCircle.from3Points = staticmethod(QgsCircle.from3Points) @@ -8,6 +24,10 @@ QgsCircle.from3TangentsMulti = staticmethod(QgsCircle.from3TangentsMulti) QgsCircle.fromExtent = staticmethod(QgsCircle.fromExtent) QgsCircle.minimalCircleFrom3Points = staticmethod(QgsCircle.minimalCircleFrom3Points) + QgsCircle.calculateSegments = staticmethod(QgsCircle.calculateSegments) + QgsCircle.calculateSegmentsAdaptive = staticmethod(QgsCircle.calculateSegmentsAdaptive) + QgsCircle.calculateSegmentsByAreaError = staticmethod(QgsCircle.calculateSegmentsByAreaError) + QgsCircle.calculateSegmentsByConstant = staticmethod(QgsCircle.calculateSegmentsByConstant) QgsCircle.__group__ = ['geometry'] except (NameError, AttributeError): pass diff --git a/python/core/auto_generated/geometry/qgscircle.sip.in b/python/core/auto_generated/geometry/qgscircle.sip.in index 8cbcbb8a36f8..63b1d62dd548 100644 --- a/python/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/core/auto_generated/geometry/qgscircle.sip.in @@ -14,6 +14,7 @@ + class QgsCircle : QgsEllipse { %Docstring(signature="appended") @@ -89,11 +90,7 @@ The azimuth is the angle between ``center`` and ``pt1``. %End - static QgsCircle from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, - const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, - const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, - double epsilon = 1E-8, - const QgsPoint &pos = QgsPoint() ) /HoldGIL/; + static QgsCircle from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, double epsilon = 1E-8, const QgsPoint &pos = QgsPoint() ) /HoldGIL/; %Docstring Constructs a circle by 3 tangents on the circle (aka inscribed circle of a triangle). Z and m values are dropped for the center point. @@ -125,11 +122,7 @@ Example # %End - static QVector from3TangentsMulti( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, - const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, - const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, - double epsilon = 1E-8, - const QgsPoint &pos = QgsPoint() ) /HoldGIL/; + static QVector from3TangentsMulti( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, double epsilon = 1E-8, const QgsPoint &pos = QgsPoint() ) /HoldGIL/; %Docstring Returns an array of circle constructed by 3 tangents on the circle (aka inscribed circle of a triangle). @@ -233,9 +226,7 @@ If found, the tangent points will be stored in ``pt1`` and ``pt2``. .. versionadded:: 3.2 %End - int outerTangents( const QgsCircle &other, - QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, - QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; + int outerTangents( const QgsCircle &other, QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; %Docstring Calculates the outer tangent points between this circle and an ``other`` circle. @@ -262,9 +253,7 @@ Returns the number of tangents (either 0 or 2). .. versionadded:: 3.2 %End - int innerTangents( const QgsCircle &other, - QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, - QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; + int innerTangents( const QgsCircle &other, QgsPointXY &line1P1 /Out/, QgsPointXY &line1P2 /Out/, QgsPointXY &line2P1 /Out/, QgsPointXY &line2P2 /Out/ ) const; %Docstring Calculates the inner tangent points between this circle and an ``other`` circle. @@ -388,11 +377,189 @@ Coordinates are taken from quadrant North, East and South. .. seealso:: :py:func:`asGml2` %End + enum class SegmentCalculationMethod + { + Standard, + Adaptive, + AreaError, + ConstantDensity + }; + + static int calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ); +%Docstring +Calculates the number of segments needed to approximate a circle. + +:param radius: Circle radius. Must be positive; if <= 0, `minSegments` is returned. +:param parameter: Maximum tolerance allowed for the deviation between the circle and its approximation, + except for the ConstantDensity method where it is a constant. If <= 0, a default value of 0.01 is used. +:param minSegments: Minimum number of segments to use. If < 3, it is set to 3. +:param method: Calculation method to use. + +:return: Number of segments needed for the approximation. + +\pre `radius` must be strictly positive; otherwise, the function returns `minSegments`. +\pre `parameter` should be positive; if not, it defaults to 0.01. +\pre `minSegments` should be at least 3; if less, it is clamped to 3. + +.. versionadded:: 3.44 +%End + + SIP_PYOBJECT __repr__(); %MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->toString() ); + QString str + = QStringLiteral( "" ).arg( sipCpp->toString() ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); %End + + private : + + static int +%Docstring +Calculate the number of segments needed to approximate a circle within a given tolerance. + +This function uses the sagitta (geometric chord height) to determine the number of segments +required to approximate a circle such that the maximum deviation between the circle and its +polygonal approximation is less than the specified tolerance. + +Mathematical approach: +1. Using the sagitta formula: s = r(1 - cos(θ/2)) +where s is the sagitta, r is the radius, and θ is the segment angle +2. Substituting tolerance for s: +tolerance = radius(1 - cos(θ/2)) +3. Solving for θ: +tolerance/radius = 1 - cos(θ/2) +cos(θ/2) = 1 - tolerance/radius +θ/2 = arccos(1 - tolerance/radius) +θ = 2 * arccos(1 - tolerance/radius) +4. Number of segments = ceil(2π / θ) += ceil(π / arccos(1 - tolerance/radius)) + +:param radius: The radius of the circle to approximate +:param tolerance: Maximum allowed deviation between the circle and its polygonal approximation +:param minSegments: Minimum number of segments to use, regardless of the calculated value + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End + calculateSegmentsStandard( double radius, double tolerance, int minSegments ); + + static int calculateSegmentsAdaptive( double radius, double tolerance, int minSegments ); +%Docstring +Calculate the number of segments with adaptive tolerance based on radius. + +This method extends :py:func:`~QgsCircle.calculateSegments` by using an adaptive tolerance that scales +with the radius to maintain better visual quality. While :py:func:`~QgsCircle.calculateSegments` uses +a fixed tolerance, this version adjusts the tolerance based on the radius size. + +Mathematical approach: +1. Compute adaptive tolerance that varies with radius: +adaptive_tolerance = base_tolerance * sqrt(radius) / log10(radius + 1) + +- For small radii: tolerance decreases → more segments for better detail +- For large radii: tolerance increases gradually → fewer segments needed +- sqrt(radius) provides basic scaling +- log10(radius + 1) dampens the scaling for large radii + +2. Apply sagitta-based calculation: + +- Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) +- Number of segments = ceil(2π/angle) + +This adaptation ensures: + +- Small circles get more segments for better visual quality +- Large circles don't get excessive segments +- Smooth transition between different scales + +:param radius: The radius of the circle to approximate +:param tolerance: Base tolerance value that will be scaled +:param minSegments: Minimum number of segments to use + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End + + static int calculateSegmentsByAreaError( double radius, double baseTolerance, int minSegments ); +%Docstring +Calculate the number of segments based on the maximum allowed area error. + +This function computes the minimum number of segments needed to approximate +a circle with a regular polygon such that the relative area error between +the polygonal approximation and the actual circle is less than the specified tolerance. + +Mathematical derivation: +1. Area ratio between a regular n-sided polygon and a circle: + +- Circle area: Ac = πr² +- Regular polygon area: Ap = (nr²/2) * sin(2π/n) +- Ratio = Ap / Ac = (n / 2π) * sin(2π/n) + +2. For relative error E: +E = |1 - Ap / Ac| = |1 - (n / 2π) * sin(2π/n)| + +3. Using Taylor series approximation for sin(x) when x is small: +sin(x) ≈ x - x³ / 6 +With x = 2π / n: +sin(2π / n) ≈ (2π / n) - (2π / n)³ / 6 + +4. Substituting and simplifying: +E ≈ |1 - (n / 2π) * ((2π / n) - (2π / n)³ / 6)| +E ≈ |1 - (1 - (2π² / 3n²))| +E ≈ 2π² / 3n² + +5. Rearranging to find the minimum n for a given tolerance: + +- Start with the inequality: E ≤ tolerance +- Substitute the expression for E: + 2π² / 3n² ≤ tolerance +- Rearrange to isolate n²: + n² ≥ 2π² / (3 * tolerance) +- Taking the square root: + n ≥ π * sqrt(2 / (3 * tolerance)) + +:param radius: The radius of the circle to approximate +:param tolerance: Maximum acceptable area error in percentage +:param minSegments: The minimum number of segments to use + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End + + static int calculateSegmentsByConstant( double radius, double constant, int minSegments ); +%Docstring +Calculate the number of segments using a simple linear relationship with radius. + +This function implements the simplest approach to circle discretization by using +a direct linear relationship between the radius and the number of segments. +While not mathematically precise for error control, it provides a quick and +intuitive approximation that can be useful when exact error bounds aren't required. + +Mathematical approach: +1. Linear scaling: segments = constant * radius + +- Larger constant = more segments = better approximation +- Smaller constant = fewer segments = coarser approximation + +:param radius: The radius of the circle to approximate +:param constant: Multiplier that determines the density of segments +:param minSegments: The minimum number of segments to use + +:return: The number of segments needed + +.. note:: + + This is a private helper method +%End }; /************************************************************************ diff --git a/src/core/geometry/qgscircle.cpp b/src/core/geometry/qgscircle.cpp index 0dfcd7f2c578..edf6d309f4e5 100644 --- a/src/core/geometry/qgscircle.cpp +++ b/src/core/geometry/qgscircle.cpp @@ -23,16 +23,14 @@ #include #include -QgsCircle::QgsCircle() : - QgsEllipse( QgsPoint(), 0.0, 0.0, 0.0 ) +QgsCircle::QgsCircle() + : QgsEllipse( QgsPoint(), 0.0, 0.0, 0.0 ) { - } -QgsCircle::QgsCircle( const QgsPoint ¢er, double radius, double azimuth ) : - QgsEllipse( center, radius, radius, azimuth ) +QgsCircle::QgsCircle( const QgsPoint ¢er, double radius, double azimuth ) + : QgsEllipse( center, radius, radius, azimuth ) { - } QgsCircle QgsCircle::from2Points( const QgsPoint &pt1, const QgsPoint &pt2 ) @@ -78,7 +76,6 @@ static bool isPerpendicular( const QgsPoint &pt1, const QgsPoint &pt2, const Qgs } return false; - } QgsCircle QgsCircle::from3Points( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3, double epsilon ) @@ -159,14 +156,10 @@ QgsCircle QgsCircle::from3Points( const QgsPoint &pt1, const QgsPoint &pt2, cons } center.setX( - ( aSlope * bSlope * ( p1.y() - p3.y() ) + - bSlope * ( p1.x() + p2.x() ) - - aSlope * ( p2.x() + p3.x() ) ) / - ( 2.0 * ( bSlope - aSlope ) ) + ( aSlope * bSlope * ( p1.y() - p3.y() ) + bSlope * ( p1.x() + p2.x() ) - aSlope * ( p2.x() + p3.x() ) ) / ( 2.0 * ( bSlope - aSlope ) ) ); center.setY( - -1.0 * ( center.x() - ( p1.x() + p2.x() ) / 2.0 ) / - aSlope + ( p1.y() + p2.y() ) / 2.0 + -1.0 * ( center.x() - ( p1.x() + p2.x() ) / 2.0 ) / aSlope + ( p1.y() + p2.y() ) / 2.0 ); radius = center.distance( p1 ); @@ -179,7 +172,7 @@ QgsCircle QgsCircle::fromCenterDiameter( const QgsPoint ¢er, double diameter return QgsCircle( center, diameter / 2.0, azimuth ); } -QgsCircle QgsCircle::fromCenterPoint( const QgsPoint ¢er, const QgsPoint &pt1 ) // cppcheck-suppress duplInheritedMember +QgsCircle QgsCircle::fromCenterPoint( const QgsPoint ¢er, const QgsPoint &pt1 ) // cppcheck-suppress duplInheritedMember { const double azimuth = QgsGeometryUtilsBase::lineAngle( center.x(), center.y(), pt1.x(), pt1.y() ) * 180.0 / M_PI; @@ -346,9 +339,7 @@ int QgsCircle::intersections( const QgsCircle &other, QgsPoint &intersection1, Q QgsPointXY int1, int2; - const int res = QgsGeometryUtils::circleCircleIntersections( QgsPointXY( mCenter ), radius(), - QgsPointXY( other.center() ), other.radius(), - int1, int2 ); + const int res = QgsGeometryUtils::circleCircleIntersections( QgsPointXY( mCenter ), radius(), QgsPointXY( other.center() ), other.radius(), int1, int2 ); if ( res == 0 ) return 0; @@ -369,14 +360,12 @@ bool QgsCircle::tangentToPoint( const QgsPointXY &p, QgsPointXY &pt1, QgsPointXY int QgsCircle::outerTangents( const QgsCircle &other, QgsPointXY &line1P1, QgsPointXY &line1P2, QgsPointXY &line2P1, QgsPointXY &line2P2 ) const { - return QgsGeometryUtils::circleCircleOuterTangents( QgsPointXY( mCenter ), radius(), - QgsPointXY( other.center() ), other.radius(), line1P1, line1P2, line2P1, line2P2 ); + return QgsGeometryUtils::circleCircleOuterTangents( QgsPointXY( mCenter ), radius(), QgsPointXY( other.center() ), other.radius(), line1P1, line1P2, line2P1, line2P2 ); } int QgsCircle::innerTangents( const QgsCircle &other, QgsPointXY &line1P1, QgsPointXY &line1P2, QgsPointXY &line2P1, QgsPointXY &line2P2 ) const { - return QgsGeometryUtils::circleCircleInnerTangents( QgsPointXY( mCenter ), radius(), - QgsPointXY( other.center() ), other.radius(), line1P1, line1P2, line2P1, line2P2 ); + return QgsGeometryUtils::circleCircleInnerTangents( QgsPointXY( mCenter ), radius(), QgsPointXY( other.center() ), other.radius(), line1P1, line1P2, line2P1, line2P2 ); } QgsCircle QgsCircle::fromExtent( const QgsPoint &pt1, const QgsPoint &pt2 ) // cppcheck-suppress duplInheritedMember @@ -472,7 +461,6 @@ QString QgsCircle::toString( int pointPrecision, int radiusPrecision, int azimut .arg( qgsDoubleToString( mAzimuth, azimuthPrecision ), 0, 'f' ); return rep; - } QDomElement QgsCircle::asGml2( QDomDocument &doc, int precision, const QString &ns, const QgsAbstractGeometry::AxisOrder axisOrder ) const @@ -496,3 +484,35 @@ QDomElement QgsCircle::asGml3( QDomDocument &doc, int precision, const QString & elemCircle.appendChild( QgsGeometryUtils::pointsToGML3( pts, doc, precision, ns, mCenter.is3D(), axisOrder ) ); return elemCircle; } + +int QgsCircle::calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ) +{ + if ( radius <= 0.0 ) + { + return minSegments; + } + + if ( parameter <= 0.0 ) + { + parameter = 0.01; + } + + if ( minSegments < 3 ) + { + minSegments = 3; + } + + switch ( method ) + { + case SegmentCalculationMethod::Standard: + return calculateSegmentsStandard( radius, parameter, minSegments ); + case SegmentCalculationMethod::Adaptive: + return calculateSegmentsAdaptive( radius, parameter, minSegments ); + case SegmentCalculationMethod::AreaError: + return calculateSegmentsByAreaError( radius, parameter, minSegments ); + case SegmentCalculationMethod::ConstantDensity: + return calculateSegmentsByConstant( radius, parameter, minSegments ); + default: + return calculateSegmentsStandard( radius, parameter, minSegments ); + } +} diff --git a/src/core/geometry/qgscircle.h b/src/core/geometry/qgscircle.h index 4ce628edc39e..df0ace2f9788 100644 --- a/src/core/geometry/qgscircle.h +++ b/src/core/geometry/qgscircle.h @@ -20,6 +20,8 @@ #include +#include + #include "qgis_core.h" #include "qgsellipse.h" #include "qgspolygon.h" @@ -127,11 +129,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse * # * \endcode */ - static QgsCircle from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, - const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, - const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, - double epsilon = 1E-8, - const QgsPoint &pos = QgsPoint() ) SIP_HOLDGIL; + static QgsCircle from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, double epsilon = 1E-8, const QgsPoint &pos = QgsPoint() ) SIP_HOLDGIL; /** * Returns an array of circle constructed by 3 tangents on the circle (aka inscribed circle of a triangle). @@ -173,11 +171,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse * # [] * \endcode */ - static QVector from3TangentsMulti( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, - const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, - const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, - double epsilon = 1E-8, - const QgsPoint &pos = QgsPoint() ) SIP_HOLDGIL; + static QVector from3TangentsMulti( const QgsPoint &pt1_tg1, const QgsPoint &pt2_tg1, const QgsPoint &pt1_tg2, const QgsPoint &pt2_tg2, const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3, double epsilon = 1E-8, const QgsPoint &pos = QgsPoint() ) SIP_HOLDGIL; /** * Constructs a circle by an extent (aka bounding box / QgsRectangle). @@ -255,9 +249,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse * \see innerTangents() * \since QGIS 3.2 */ - int outerTangents( const QgsCircle &other, - QgsPointXY &line1P1 SIP_OUT, QgsPointXY &line1P2 SIP_OUT, - QgsPointXY &line2P1 SIP_OUT, QgsPointXY &line2P2 SIP_OUT ) const; + int outerTangents( const QgsCircle &other, QgsPointXY &line1P1 SIP_OUT, QgsPointXY &line1P2 SIP_OUT, QgsPointXY &line2P1 SIP_OUT, QgsPointXY &line2P2 SIP_OUT ) const; /** * Calculates the inner tangent points between this circle @@ -280,9 +272,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse * \see outerTangents() * \since QGIS 3.6 */ - int innerTangents( const QgsCircle &other, - QgsPointXY &line1P1 SIP_OUT, QgsPointXY &line1P2 SIP_OUT, - QgsPointXY &line2P1 SIP_OUT, QgsPointXY &line2P2 SIP_OUT ) const; + int innerTangents( const QgsCircle &other, QgsPointXY &line1P1 SIP_OUT, QgsPointXY &line1P2 SIP_OUT, QgsPointXY &line2P1 SIP_OUT, QgsPointXY &line2P2 SIP_OUT ) const; double area() const override SIP_HOLDGIL; double perimeter() const override SIP_HOLDGIL; @@ -307,7 +297,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse void setSemiMinorAxis( double semiMinorAxis ) override SIP_HOLDGIL; //! Returns the radius of the circle - double radius() const SIP_HOLDGIL {return mSemiMajorAxis;} + double radius() const SIP_HOLDGIL { return mSemiMajorAxis; } //! Sets the radius of the circle void setRadius( double radius ) SIP_HOLDGIL { @@ -366,13 +356,221 @@ class CORE_EXPORT QgsCircle : public QgsEllipse */ QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + /** + * \brief Method used to calculate the number of segments for circle approximation + * \ingroup core + * \since QGIS 3.44 + */ + enum class SegmentCalculationMethod + { + Standard = 0, //!< Standard sagitta-based calculation + Adaptive, //!< Adaptive calculation based on radius size + AreaError, //!< Calculation based on area error + ConstantDensity //!< Simple calculation with constant segment density + }; + + /** + * Calculates the number of segments needed to approximate a circle. + * + * \param radius Circle radius. Must be positive; if <= 0, `minSegments` is returned. + * \param parameter Maximum tolerance allowed for the deviation between the circle and its approximation, + * except for the ConstantDensity method where it is a constant. If <= 0, a default value of 0.01 is used. + * \param minSegments Minimum number of segments to use. If < 3, it is set to 3. + * \param method Calculation method to use. + * \returns Number of segments needed for the approximation. + * + * \pre `radius` must be strictly positive; otherwise, the function returns `minSegments`. + * \pre `parameter` should be positive; if not, it defaults to 0.01. + * \pre `minSegments` should be at least 3; if less, it is clamped to 3. + * + * \since QGIS 3.44 + */ + static int calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ); + + #ifdef SIP_RUN SIP_PYOBJECT __repr__(); % MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->toString() ); + QString str + = QStringLiteral( "" ).arg( sipCpp->toString() ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); % End #endif + + private : + + /** + * Calculate the number of segments needed to approximate a circle within a given tolerance. + * + * This function uses the sagitta (geometric chord height) to determine the number of segments + * required to approximate a circle such that the maximum deviation between the circle and its + * polygonal approximation is less than the specified tolerance. + * + * Mathematical approach: + * 1. Using the sagitta formula: s = r(1 - cos(θ/2)) + * where s is the sagitta, r is the radius, and θ is the segment angle + * 2. Substituting tolerance for s: + * tolerance = radius(1 - cos(θ/2)) + * 3. Solving for θ: + * tolerance/radius = 1 - cos(θ/2) + * cos(θ/2) = 1 - tolerance/radius + * θ/2 = arccos(1 - tolerance/radius) + * θ = 2 * arccos(1 - tolerance/radius) + * 4. Number of segments = ceil(2π / θ) + * = ceil(π / arccos(1 - tolerance/radius)) + * + * \param radius The radius of the circle to approximate + * \param tolerance Maximum allowed deviation between the circle and its polygonal approximation + * \param minSegments Minimum number of segments to use, regardless of the calculated value + * \returns The number of segments needed + * \note This is a private helper method + */ + static int + calculateSegmentsStandard( double radius, double tolerance, int minSegments ) + { + if ( tolerance >= radius ) + { + return minSegments; + } + + // Using the sagitta formula: s = r(1 - cos(θ/2)) + const double halfAngle = std::acos( 1.0 - tolerance / radius ); + const int segments = std::ceil( M_PI / halfAngle ); + + return std::max( segments, minSegments ); + } + + /** + * Calculate the number of segments with adaptive tolerance based on radius. + * + * This method extends calculateSegments() by using an adaptive tolerance that scales + * with the radius to maintain better visual quality. While calculateSegments() uses + * a fixed tolerance, this version adjusts the tolerance based on the radius size. + * + * Mathematical approach: + * 1. Compute adaptive tolerance that varies with radius: + * adaptive_tolerance = base_tolerance * sqrt(radius) / log10(radius + 1) + * + * - For small radii: tolerance decreases → more segments for better detail + * - For large radii: tolerance increases gradually → fewer segments needed + * - sqrt(radius) provides basic scaling + * - log10(radius + 1) dampens the scaling for large radii + * + * 2. Apply sagitta-based calculation: + * + * - Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) + * - Number of segments = ceil(2π/angle) + * + * This adaptation ensures: + * + * - Small circles get more segments for better visual quality + * - Large circles don't get excessive segments + * - Smooth transition between different scales + * + * \param radius The radius of the circle to approximate + * \param tolerance Base tolerance value that will be scaled + * \param minSegments Minimum number of segments to use + * \returns The number of segments needed + * \note This is a private helper method + */ + static int calculateSegmentsAdaptive( double radius, double tolerance, int minSegments ) + { + // Compute adaptive tolerance that varies with radius + const double adaptiveTolerance = tolerance * std::sqrt( radius ) / std::log10( radius + 1.0 ); + + if ( adaptiveTolerance >= radius ) + { + return minSegments; + } + + const double halfAngle = std::acos( 1.0 - adaptiveTolerance / radius ); + const int segments = std::ceil( M_PI / halfAngle ); + + return std::max( segments, minSegments ); + } + + /** + * Calculate the number of segments based on the maximum allowed area error. + * + * This function computes the minimum number of segments needed to approximate + * a circle with a regular polygon such that the relative area error between + * the polygonal approximation and the actual circle is less than the specified tolerance. + * + * Mathematical derivation: + * 1. Area ratio between a regular n-sided polygon and a circle: + * + * - Circle area: Ac = πr² + * - Regular polygon area: Ap = (nr²/2) * sin(2π/n) + * - Ratio = Ap / Ac = (n / 2π) * sin(2π/n) + * + * 2. For relative error E: + * E = |1 - Ap / Ac| = |1 - (n / 2π) * sin(2π/n)| + * + * 3. Using Taylor series approximation for sin(x) when x is small: + * sin(x) ≈ x - x³ / 6 + * With x = 2π / n: + * sin(2π / n) ≈ (2π / n) - (2π / n)³ / 6 + * + * 4. Substituting and simplifying: + * E ≈ |1 - (n / 2π) * ((2π / n) - (2π / n)³ / 6)| + * E ≈ |1 - (1 - (2π² / 3n²))| + * E ≈ 2π² / 3n² + * + * 5. Rearranging to find the minimum n for a given tolerance: + * + * - Start with the inequality: E ≤ tolerance + * - Substitute the expression for E: + * 2π² / 3n² ≤ tolerance + * - Rearrange to isolate n²: + * n² ≥ 2π² / (3 * tolerance) + * - Taking the square root: + * n ≥ π * sqrt(2 / (3 * tolerance)) + * + * \param radius The radius of the circle to approximate + * \param tolerance Maximum acceptable area error in percentage + * \param minSegments The minimum number of segments to use + * \returns The number of segments needed + * \note This is a private helper method + */ + static int calculateSegmentsByAreaError( double radius, double baseTolerance, int minSegments ) + { + Q_UNUSED( radius ); + // Convert tolerance from percentage to decimal + const double decimalTolerance = baseTolerance / 100.0; + + // Avoid division by zero or extremely small tolerance + const double tolerance = std::max( decimalTolerance, 1.0e-8 ); + + // Calculate required segments using the area error formula + const double requiredSegments = M_PI * std::sqrt( 2.0 / ( 3.0 * tolerance ) ); + + return std::max( static_cast( std::ceil( requiredSegments ) ), minSegments ); + } + + /** + * Calculate the number of segments using a simple linear relationship with radius. + * + * This function implements the simplest approach to circle discretization by using + * a direct linear relationship between the radius and the number of segments. + * While not mathematically precise for error control, it provides a quick and + * intuitive approximation that can be useful when exact error bounds aren't required. + * + * Mathematical approach: + * 1. Linear scaling: segments = constant * radius + * + * - Larger constant = more segments = better approximation + * - Smaller constant = fewer segments = coarser approximation + * + * \param radius The radius of the circle to approximate + * \param constant Multiplier that determines the density of segments + * \param minSegments The minimum number of segments to use + * \returns The number of segments needed + * \note This is a private helper method + */ + static int calculateSegmentsByConstant( double radius, double constant, int minSegments ) + { + return std::max( minSegments, static_cast( std::ceil( constant * radius ) ) ); + } }; #endif // QGSCIRCLE_H diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index be46cfd16125..bc5860cfe066 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -41,6 +41,7 @@ ADD_PYTHON_TEST(PyQgsCalloutPanelWidget test_qgscalloutpanelwidget.py) ADD_PYTHON_TEST(PyQgsCategorizedSymbolRenderer test_qgscategorizedsymbolrenderer.py) ADD_PYTHON_TEST(PyQgsCesium3dTilesLayer test_qgscesium3dtileslayer.py) ADD_PYTHON_TEST(PyQgsCesiumUtils test_qgscesiumutils.py) +ADD_PYTHON_TEST(PyQgsCircle test_qgscircle.py) ADD_PYTHON_TEST(PyQgsCircularString test_qgscircularstring.py) ADD_PYTHON_TEST(PyQgsClassificationMethod test_qgsclassificationmethod.py) ADD_PYTHON_TEST(PyQgsColorRamp test_qgscolorramp.py) diff --git a/tests/src/python/test_qgscircle.py b/tests/src/python/test_qgscircle.py new file mode 100644 index 000000000000..dd12c0c7ecd1 --- /dev/null +++ b/tests/src/python/test_qgscircle.py @@ -0,0 +1,132 @@ +"""QGIS Unit tests for QgsCircularString. + +.. note:: 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. +""" + +__author__ = "Loïc Bartoletti" +__date__ = "29/01/2025" +__copyright__ = "Copyright 2025, The QGIS Project" + +import qgis # NOQA + +from qgis.core import QgsCircle, QgsPoint +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsCircularString(QgisTestCase): + + def testSegmentCalculation(self): + """Test different methods for calculating segments for circle approximation""" + + # Test point (EPSG:3946) + center = QgsPoint(1981498.81943113403394818, 5199309.83504835329949856) + radius = 0.1 + + # Store results for comparison + results = [] + + # Expected values from the actual implementation + expected_values = [ + # radius, standard, adaptive, area, constant + [0.1, 8, 8, 257, 8], + [1.0, 23, 13, 257, 8], + [10.0, 71, 41, 257, 20], + [100.0, 223, 100, 257, 200], + [1000.0, 703, 217, 257, 2000], + [10000.0, 2222, 445, 257, 20000], + [100000.0, 7025, 884, 257, 200000], + [1000000.0, 22215, 1721, 257, 2000000], + ] + + while radius <= 1500000: + circle = QgsCircle(center, radius) + tolerance = 0.01 + constant = 2 + min_segments = 8 + + # Test all methods + standard_segments = circle.calculateSegments( + radius, + tolerance, + min_segments, + QgsCircle.SegmentCalculationMethod.Standard, + ) + adaptive_segments = circle.calculateSegments( + radius, + tolerance, + min_segments, + QgsCircle.SegmentCalculationMethod.Adaptive, + ) + area_segments = circle.calculateSegments( + radius, + tolerance, + min_segments, + QgsCircle.SegmentCalculationMethod.AreaError, + ) + constant_segments = circle.calculateSegments( + radius, + constant, + min_segments, + QgsCircle.SegmentCalculationMethod.ConstantDensity, + ) + + # Store current results + results.append( + [ + radius, + standard_segments, + adaptive_segments, + area_segments, + constant_segments, + ] + ) + + # Find corresponding expected values + expected = next( + val for val in expected_values if abs(val[0] - radius) < 0.01 + ) + + # Test against expected values + self.assertEqual( + standard_segments, + expected[1], + f"Standard method for radius {radius} should give {expected[1]} segments", + ) + self.assertEqual( + adaptive_segments, + expected[2], + f"Adaptive method for radius {radius} should give {expected[2]} segments", + ) + self.assertEqual( + area_segments, + expected[3], + f"Area method for radius {radius} should give {expected[3]} segments", + ) + self.assertEqual( + constant_segments, + expected[4], + f"Constant method for radius {radius} should give {expected[4]} segments", + ) + + radius *= 10 + + # Display results table + print("\nSegment calculation results:") + print( + "{:<12} {:<12} {:<12} {:<12} {:<12}".format( + "Radius", "Standard", "Adaptive", "Area", "Constant" + ) + ) + print("-" * 60) + for result in results: + print("{:<12.2f} {:<12d} {:<12d} {:<12d} {:<12d}".format(*result)) + + +if __name__ == "__main__": + unittest.main() From 07e8f666669a34c3de3ceee043acc4523adfa12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Wed, 5 Feb 2025 19:19:25 +0100 Subject: [PATCH 2/4] move SegmentCalculationMethod to qgis.h --- python/PyQt6/core/auto_additions/qgis.py | 17 +++++++++++++++++ python/PyQt6/core/auto_additions/qgscircle.py | 17 +---------------- .../auto_generated/geometry/qgscircle.sip.in | 13 ++----------- python/PyQt6/core/auto_generated/qgis.sip.in | 8 ++++++++ python/core/auto_additions/qgis.py | 17 +++++++++++++++++ python/core/auto_additions/qgscircle.py | 17 +---------------- .../auto_generated/geometry/qgscircle.sip.in | 13 ++----------- python/core/auto_generated/qgis.sip.in | 8 ++++++++ src/core/geometry/qgscircle.cpp | 10 +++++----- src/core/geometry/qgscircle.h | 18 ++---------------- src/core/qgis.h | 13 +++++++++++++ tests/src/python/test_qgscircle.py | 10 +++++----- 12 files changed, 81 insertions(+), 80 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 3f0acb5a87d7..28a4da2216dc 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -11161,6 +11161,23 @@ """ # -- Qgis.PointCloudZoomOutRenderBehavior.baseClass = Qgis +# monkey patching scoped based enum +Qgis.SegmentCalculationMethod.Standard.__doc__ = "Standard sagitta-based calculation" +Qgis.SegmentCalculationMethod.Adaptive.__doc__ = "Adaptive calculation based on radius size" +Qgis.SegmentCalculationMethod.AreaError.__doc__ = "Calculation based on area error" +Qgis.SegmentCalculationMethod.ConstantDensity.__doc__ = "Simple calculation with constant segment density" +Qgis.SegmentCalculationMethod.__doc__ = """brief Method used to calculate the number of segments for circle approximation + +.. versionadded:: 3.44 + +* ``Standard``: Standard sagitta-based calculation +* ``Adaptive``: Adaptive calculation based on radius size +* ``AreaError``: Calculation based on area error +* ``ConstantDensity``: Simple calculation with constant segment density + +""" +# -- +Qgis.SegmentCalculationMethod.baseClass = Qgis try: Qgis.__attribute_docs__ = {'QGIS_DEV_VERSION': 'The development version', 'DEFAULT_SEARCH_RADIUS_MM': 'Identify search radius in mm', 'DEFAULT_MAPTOPIXEL_THRESHOLD': 'Default threshold between map coordinates and device coordinates for map2pixel simplification', 'DEFAULT_HIGHLIGHT_COLOR': 'Default highlight color. The transparency is expected to only be applied to polygon\nfill. Lines and outlines are rendered opaque.', 'DEFAULT_HIGHLIGHT_BUFFER_MM': 'Default highlight buffer in mm.', 'DEFAULT_HIGHLIGHT_MIN_WIDTH_MM': 'Default highlight line/stroke minimum width in mm.', 'SCALE_PRECISION': 'Fudge factor used to compare two scales. The code is often going from scale to scale\ndenominator. So it looses precision and, when a limit is inclusive, can lead to errors.\nTo avoid that, use this factor instead of using <= or >=.\n\n.. deprecated:: 3.40\n\n No longer used by QGIS and will be removed in QGIS 4.0.', 'DEFAULT_Z_COORDINATE': 'Default Z coordinate value.\nThis value have to be assigned to the Z coordinate for the vertex.', 'DEFAULT_M_COORDINATE': 'Default M coordinate value.\nThis value have to be assigned to the M coordinate for the vertex.\n\n.. versionadded:: 3.20', 'UI_SCALE_FACTOR': 'UI scaling factor. This should be applied to all widget sizes obtained from font metrics,\nto account for differences in the default font sizes across different platforms.', 'DEFAULT_SNAP_TOLERANCE': 'Default snapping distance tolerance.', 'DEFAULT_SNAP_UNITS': 'Default snapping distance units.'} Qgis.version = staticmethod(Qgis.version) diff --git a/python/PyQt6/core/auto_additions/qgscircle.py b/python/PyQt6/core/auto_additions/qgscircle.py index 05b32ed8dfba..78dcca726e57 100644 --- a/python/PyQt6/core/auto_additions/qgscircle.py +++ b/python/PyQt6/core/auto_additions/qgscircle.py @@ -1,20 +1,4 @@ # The following has been generated automatically from src/core/geometry/qgscircle.h -# monkey patching scoped based enum -QgsCircle.SegmentCalculationMethod.Standard.__doc__ = "Standard sagitta-based calculation" -QgsCircle.SegmentCalculationMethod.Adaptive.__doc__ = "Adaptive calculation based on radius size" -QgsCircle.SegmentCalculationMethod.AreaError.__doc__ = "Calculation based on area error" -QgsCircle.SegmentCalculationMethod.ConstantDensity.__doc__ = "Simple calculation with constant segment density" -QgsCircle.SegmentCalculationMethod.__doc__ = """Method used to calculate the number of segments for circle approximation - -.. versionadded:: 3.44 - -* ``Standard``: Standard sagitta-based calculation -* ``Adaptive``: Adaptive calculation based on radius size -* ``AreaError``: Calculation based on area error -* ``ConstantDensity``: Simple calculation with constant segment density - -""" -# -- try: QgsCircle.from2Points = staticmethod(QgsCircle.from2Points) QgsCircle.from3Points = staticmethod(QgsCircle.from3Points) @@ -25,6 +9,7 @@ QgsCircle.fromExtent = staticmethod(QgsCircle.fromExtent) QgsCircle.minimalCircleFrom3Points = staticmethod(QgsCircle.minimalCircleFrom3Points) QgsCircle.calculateSegments = staticmethod(QgsCircle.calculateSegments) + QgsCircle.calculateSegmentsStandard = staticmethod(QgsCircle.calculateSegmentsStandard) QgsCircle.calculateSegmentsAdaptive = staticmethod(QgsCircle.calculateSegmentsAdaptive) QgsCircle.calculateSegmentsByAreaError = staticmethod(QgsCircle.calculateSegmentsByAreaError) QgsCircle.calculateSegmentsByConstant = staticmethod(QgsCircle.calculateSegmentsByConstant) diff --git a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in index 63b1d62dd548..501b91fb1f22 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in @@ -377,15 +377,7 @@ Coordinates are taken from quadrant North, East and South. .. seealso:: :py:func:`asGml2` %End - enum class SegmentCalculationMethod - { - Standard, - Adaptive, - AreaError, - ConstantDensity - }; - - static int calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ); + static int calculateSegments( double radius, double parameter, int minSegments, Qgis::SegmentCalculationMethod method ); %Docstring Calculates the number of segments needed to approximate a circle. @@ -414,7 +406,7 @@ Calculates the number of segments needed to approximate a circle. private : - static int + static int calculateSegmentsStandard( double radius, double tolerance, int minSegments ); %Docstring Calculate the number of segments needed to approximate a circle within a given tolerance. @@ -445,7 +437,6 @@ cos(θ/2) = 1 - tolerance/radius This is a private helper method %End - calculateSegmentsStandard( double radius, double tolerance, int minSegments ); static int calculateSegmentsAdaptive( double radius, double tolerance, int minSegments ); %Docstring diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 2c8ae9e630eb..1e0c27a9c2ee 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -3271,6 +3271,14 @@ The development version RenderOverviewAndExtents }; + enum class SegmentCalculationMethod /BaseType=IntEnum/ + { + Standard, + Adaptive, + AreaError, + ConstantDensity + }; + static const double DEFAULT_SEARCH_RADIUS_MM; static const float DEFAULT_MAPTOPIXEL_THRESHOLD; diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index a45fb29ec11d..ce7a57dbe506 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -11070,6 +11070,23 @@ """ # -- Qgis.PointCloudZoomOutRenderBehavior.baseClass = Qgis +# monkey patching scoped based enum +Qgis.SegmentCalculationMethod.Standard.__doc__ = "Standard sagitta-based calculation" +Qgis.SegmentCalculationMethod.Adaptive.__doc__ = "Adaptive calculation based on radius size" +Qgis.SegmentCalculationMethod.AreaError.__doc__ = "Calculation based on area error" +Qgis.SegmentCalculationMethod.ConstantDensity.__doc__ = "Simple calculation with constant segment density" +Qgis.SegmentCalculationMethod.__doc__ = """brief Method used to calculate the number of segments for circle approximation + +.. versionadded:: 3.44 + +* ``Standard``: Standard sagitta-based calculation +* ``Adaptive``: Adaptive calculation based on radius size +* ``AreaError``: Calculation based on area error +* ``ConstantDensity``: Simple calculation with constant segment density + +""" +# -- +Qgis.SegmentCalculationMethod.baseClass = Qgis from enum import Enum diff --git a/python/core/auto_additions/qgscircle.py b/python/core/auto_additions/qgscircle.py index 05b32ed8dfba..78dcca726e57 100644 --- a/python/core/auto_additions/qgscircle.py +++ b/python/core/auto_additions/qgscircle.py @@ -1,20 +1,4 @@ # The following has been generated automatically from src/core/geometry/qgscircle.h -# monkey patching scoped based enum -QgsCircle.SegmentCalculationMethod.Standard.__doc__ = "Standard sagitta-based calculation" -QgsCircle.SegmentCalculationMethod.Adaptive.__doc__ = "Adaptive calculation based on radius size" -QgsCircle.SegmentCalculationMethod.AreaError.__doc__ = "Calculation based on area error" -QgsCircle.SegmentCalculationMethod.ConstantDensity.__doc__ = "Simple calculation with constant segment density" -QgsCircle.SegmentCalculationMethod.__doc__ = """Method used to calculate the number of segments for circle approximation - -.. versionadded:: 3.44 - -* ``Standard``: Standard sagitta-based calculation -* ``Adaptive``: Adaptive calculation based on radius size -* ``AreaError``: Calculation based on area error -* ``ConstantDensity``: Simple calculation with constant segment density - -""" -# -- try: QgsCircle.from2Points = staticmethod(QgsCircle.from2Points) QgsCircle.from3Points = staticmethod(QgsCircle.from3Points) @@ -25,6 +9,7 @@ QgsCircle.fromExtent = staticmethod(QgsCircle.fromExtent) QgsCircle.minimalCircleFrom3Points = staticmethod(QgsCircle.minimalCircleFrom3Points) QgsCircle.calculateSegments = staticmethod(QgsCircle.calculateSegments) + QgsCircle.calculateSegmentsStandard = staticmethod(QgsCircle.calculateSegmentsStandard) QgsCircle.calculateSegmentsAdaptive = staticmethod(QgsCircle.calculateSegmentsAdaptive) QgsCircle.calculateSegmentsByAreaError = staticmethod(QgsCircle.calculateSegmentsByAreaError) QgsCircle.calculateSegmentsByConstant = staticmethod(QgsCircle.calculateSegmentsByConstant) diff --git a/python/core/auto_generated/geometry/qgscircle.sip.in b/python/core/auto_generated/geometry/qgscircle.sip.in index 63b1d62dd548..501b91fb1f22 100644 --- a/python/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/core/auto_generated/geometry/qgscircle.sip.in @@ -377,15 +377,7 @@ Coordinates are taken from quadrant North, East and South. .. seealso:: :py:func:`asGml2` %End - enum class SegmentCalculationMethod - { - Standard, - Adaptive, - AreaError, - ConstantDensity - }; - - static int calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ); + static int calculateSegments( double radius, double parameter, int minSegments, Qgis::SegmentCalculationMethod method ); %Docstring Calculates the number of segments needed to approximate a circle. @@ -414,7 +406,7 @@ Calculates the number of segments needed to approximate a circle. private : - static int + static int calculateSegmentsStandard( double radius, double tolerance, int minSegments ); %Docstring Calculate the number of segments needed to approximate a circle within a given tolerance. @@ -445,7 +437,6 @@ cos(θ/2) = 1 - tolerance/radius This is a private helper method %End - calculateSegmentsStandard( double radius, double tolerance, int minSegments ); static int calculateSegmentsAdaptive( double radius, double tolerance, int minSegments ); %Docstring diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index f6e2c40a25db..48b8be7b6c05 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -3271,6 +3271,14 @@ The development version RenderOverviewAndExtents }; + enum class SegmentCalculationMethod + { + Standard, + Adaptive, + AreaError, + ConstantDensity + }; + static const double DEFAULT_SEARCH_RADIUS_MM; static const float DEFAULT_MAPTOPIXEL_THRESHOLD; diff --git a/src/core/geometry/qgscircle.cpp b/src/core/geometry/qgscircle.cpp index edf6d309f4e5..e99b8c8f1158 100644 --- a/src/core/geometry/qgscircle.cpp +++ b/src/core/geometry/qgscircle.cpp @@ -485,7 +485,7 @@ QDomElement QgsCircle::asGml3( QDomDocument &doc, int precision, const QString & return elemCircle; } -int QgsCircle::calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ) +int QgsCircle::calculateSegments( double radius, double parameter, int minSegments, Qgis::SegmentCalculationMethod method ) { if ( radius <= 0.0 ) { @@ -504,13 +504,13 @@ int QgsCircle::calculateSegments( double radius, double parameter, int minSegmen switch ( method ) { - case SegmentCalculationMethod::Standard: + case Qgis::SegmentCalculationMethod::Standard: return calculateSegmentsStandard( radius, parameter, minSegments ); - case SegmentCalculationMethod::Adaptive: + case Qgis::SegmentCalculationMethod::Adaptive: return calculateSegmentsAdaptive( radius, parameter, minSegments ); - case SegmentCalculationMethod::AreaError: + case Qgis::SegmentCalculationMethod::AreaError: return calculateSegmentsByAreaError( radius, parameter, minSegments ); - case SegmentCalculationMethod::ConstantDensity: + case Qgis::SegmentCalculationMethod::ConstantDensity: return calculateSegmentsByConstant( radius, parameter, minSegments ); default: return calculateSegmentsStandard( radius, parameter, minSegments ); diff --git a/src/core/geometry/qgscircle.h b/src/core/geometry/qgscircle.h index df0ace2f9788..429e16110aea 100644 --- a/src/core/geometry/qgscircle.h +++ b/src/core/geometry/qgscircle.h @@ -356,19 +356,6 @@ class CORE_EXPORT QgsCircle : public QgsEllipse */ QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; - /** - * \brief Method used to calculate the number of segments for circle approximation - * \ingroup core - * \since QGIS 3.44 - */ - enum class SegmentCalculationMethod - { - Standard = 0, //!< Standard sagitta-based calculation - Adaptive, //!< Adaptive calculation based on radius size - AreaError, //!< Calculation based on area error - ConstantDensity //!< Simple calculation with constant segment density - }; - /** * Calculates the number of segments needed to approximate a circle. * @@ -385,7 +372,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse * * \since QGIS 3.44 */ - static int calculateSegments( double radius, double parameter, int minSegments, SegmentCalculationMethod method ); + static int calculateSegments( double radius, double parameter, int minSegments, Qgis::SegmentCalculationMethod method ); #ifdef SIP_RUN @@ -425,8 +412,7 @@ class CORE_EXPORT QgsCircle : public QgsEllipse * \returns The number of segments needed * \note This is a private helper method */ - static int - calculateSegmentsStandard( double radius, double tolerance, int minSegments ) + static int calculateSegmentsStandard( double radius, double tolerance, int minSegments ) { if ( tolerance >= radius ) { diff --git a/src/core/qgis.h b/src/core/qgis.h index 701dfaa6d32f..8cff42bde3c5 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -5766,6 +5766,19 @@ class CORE_EXPORT Qgis }; Q_ENUM( PointCloudZoomOutRenderBehavior ) + /** + * brief Method used to calculate the number of segments for circle approximation + * \since QGIS 3.44 + */ + enum class SegmentCalculationMethod : int + { + Standard = 0, //!< Standard sagitta-based calculation + Adaptive, //!< Adaptive calculation based on radius size + AreaError, //!< Calculation based on area error + ConstantDensity //!< Simple calculation with constant segment density + }; + Q_ENUM( SegmentCalculationMethod ) + /** * Identify search radius in mm */ diff --git a/tests/src/python/test_qgscircle.py b/tests/src/python/test_qgscircle.py index dd12c0c7ecd1..53caa50b0767 100644 --- a/tests/src/python/test_qgscircle.py +++ b/tests/src/python/test_qgscircle.py @@ -12,7 +12,7 @@ import qgis # NOQA -from qgis.core import QgsCircle, QgsPoint +from qgis.core import Qgis, QgsCircle, QgsPoint import unittest from qgis.testing import start_app, QgisTestCase @@ -55,25 +55,25 @@ def testSegmentCalculation(self): radius, tolerance, min_segments, - QgsCircle.SegmentCalculationMethod.Standard, + Qgis.SegmentCalculationMethod.Standard, ) adaptive_segments = circle.calculateSegments( radius, tolerance, min_segments, - QgsCircle.SegmentCalculationMethod.Adaptive, + Qgis.SegmentCalculationMethod.Adaptive, ) area_segments = circle.calculateSegments( radius, tolerance, min_segments, - QgsCircle.SegmentCalculationMethod.AreaError, + Qgis.SegmentCalculationMethod.AreaError, ) constant_segments = circle.calculateSegments( radius, constant, min_segments, - QgsCircle.SegmentCalculationMethod.ConstantDensity, + Qgis.SegmentCalculationMethod.ConstantDensity, ) # Store current results From 7157d36b5cf8938f4d0b69da3477f3a7bdc40b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Wed, 5 Feb 2025 19:20:56 +0100 Subject: [PATCH 3/4] test_qgscircle: remove printable part --- tests/src/python/test_qgscircle.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/src/python/test_qgscircle.py b/tests/src/python/test_qgscircle.py index 53caa50b0767..d7db9a6fe735 100644 --- a/tests/src/python/test_qgscircle.py +++ b/tests/src/python/test_qgscircle.py @@ -116,17 +116,6 @@ def testSegmentCalculation(self): radius *= 10 - # Display results table - print("\nSegment calculation results:") - print( - "{:<12} {:<12} {:<12} {:<12} {:<12}".format( - "Radius", "Standard", "Adaptive", "Area", "Constant" - ) - ) - print("-" * 60) - for result in results: - print("{:<12.2f} {:<12d} {:<12d} {:<12d} {:<12d}".format(*result)) - if __name__ == "__main__": unittest.main() From 875d8062e427c37cd3ad379bb00d2ca0e3ad019d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Bartoletti?= Date: Thu, 6 Feb 2025 07:38:06 +0100 Subject: [PATCH 4/4] docs(QgsCircle): pet doxygen --- .../auto_generated/geometry/qgscircle.sip.in | 46 ++-- .../auto_generated/geometry/qgscircle.sip.in | 46 ++-- src/core/geometry/qgscircle.h | 234 +++++++++--------- 3 files changed, 160 insertions(+), 166 deletions(-) diff --git a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in index 501b91fb1f22..4fbafb8fdd31 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in @@ -450,21 +450,21 @@ Mathematical approach: 1. Compute adaptive tolerance that varies with radius: adaptive_tolerance = base_tolerance * sqrt(radius) / log10(radius + 1) -- For small radii: tolerance decreases → more segments for better detail -- For large radii: tolerance increases gradually → fewer segments needed -- sqrt(radius) provides basic scaling -- log10(radius + 1) dampens the scaling for large radii +For small radii: tolerance decreases → more segments for better detail +For large radii: tolerance increases gradually → fewer segments needed +sqrt(radius) provides basic scaling +log10(radius + 1) dampens the scaling for large radii 2. Apply sagitta-based calculation: -- Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) -- Number of segments = ceil(2π/angle) +Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) +Number of segments = ceil(2π/angle) This adaptation ensures: -- Small circles get more segments for better visual quality -- Large circles don't get excessive segments -- Smooth transition between different scales +Small circles get more segments for better visual quality +Large circles don't get excessive segments +Smooth transition between different scales :param radius: The radius of the circle to approximate :param tolerance: Base tolerance value that will be scaled @@ -487,10 +487,9 @@ the polygonal approximation and the actual circle is less than the specified tol Mathematical derivation: 1. Area ratio between a regular n-sided polygon and a circle: - -- Circle area: Ac = πr² -- Regular polygon area: Ap = (nr²/2) * sin(2π/n) -- Ratio = Ap / Ac = (n / 2π) * sin(2π/n) +Circle area: Ac = πr² +Regular polygon area: Ap = (nr²/2) * sin(2π/n) +Ratio = Ap / Ac = (n / 2π) * sin(2π/n) 2. For relative error E: E = |1 - Ap / Ac| = |1 - (n / 2π) * sin(2π/n)| @@ -506,17 +505,16 @@ E ≈ |1 - (1 - (2π² / 3n²))| E ≈ 2π² / 3n² 5. Rearranging to find the minimum n for a given tolerance: - -- Start with the inequality: E ≤ tolerance -- Substitute the expression for E: - 2π² / 3n² ≤ tolerance -- Rearrange to isolate n²: - n² ≥ 2π² / (3 * tolerance) -- Taking the square root: - n ≥ π * sqrt(2 / (3 * tolerance)) +Start with the inequality: E ≤ tolerance +Substitute the expression for E: +2π² / 3n² ≤ tolerance +Rearrange to isolate n²: +n² ≥ 2π² / (3 * tolerance) +Taking the square root: +n ≥ π * sqrt(2 / (3 * tolerance)) :param radius: The radius of the circle to approximate -:param tolerance: Maximum acceptable area error in percentage +:param baseTolerance: Maximum acceptable area error in percentage :param minSegments: The minimum number of segments to use :return: The number of segments needed @@ -538,8 +536,8 @@ intuitive approximation that can be useful when exact error bounds aren't requir Mathematical approach: 1. Linear scaling: segments = constant * radius -- Larger constant = more segments = better approximation -- Smaller constant = fewer segments = coarser approximation +Larger constant = more segments = better approximation +Smaller constant = fewer segments = coarser approximation :param radius: The radius of the circle to approximate :param constant: Multiplier that determines the density of segments diff --git a/python/core/auto_generated/geometry/qgscircle.sip.in b/python/core/auto_generated/geometry/qgscircle.sip.in index 501b91fb1f22..4fbafb8fdd31 100644 --- a/python/core/auto_generated/geometry/qgscircle.sip.in +++ b/python/core/auto_generated/geometry/qgscircle.sip.in @@ -450,21 +450,21 @@ Mathematical approach: 1. Compute adaptive tolerance that varies with radius: adaptive_tolerance = base_tolerance * sqrt(radius) / log10(radius + 1) -- For small radii: tolerance decreases → more segments for better detail -- For large radii: tolerance increases gradually → fewer segments needed -- sqrt(radius) provides basic scaling -- log10(radius + 1) dampens the scaling for large radii +For small radii: tolerance decreases → more segments for better detail +For large radii: tolerance increases gradually → fewer segments needed +sqrt(radius) provides basic scaling +log10(radius + 1) dampens the scaling for large radii 2. Apply sagitta-based calculation: -- Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) -- Number of segments = ceil(2π/angle) +Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) +Number of segments = ceil(2π/angle) This adaptation ensures: -- Small circles get more segments for better visual quality -- Large circles don't get excessive segments -- Smooth transition between different scales +Small circles get more segments for better visual quality +Large circles don't get excessive segments +Smooth transition between different scales :param radius: The radius of the circle to approximate :param tolerance: Base tolerance value that will be scaled @@ -487,10 +487,9 @@ the polygonal approximation and the actual circle is less than the specified tol Mathematical derivation: 1. Area ratio between a regular n-sided polygon and a circle: - -- Circle area: Ac = πr² -- Regular polygon area: Ap = (nr²/2) * sin(2π/n) -- Ratio = Ap / Ac = (n / 2π) * sin(2π/n) +Circle area: Ac = πr² +Regular polygon area: Ap = (nr²/2) * sin(2π/n) +Ratio = Ap / Ac = (n / 2π) * sin(2π/n) 2. For relative error E: E = |1 - Ap / Ac| = |1 - (n / 2π) * sin(2π/n)| @@ -506,17 +505,16 @@ E ≈ |1 - (1 - (2π² / 3n²))| E ≈ 2π² / 3n² 5. Rearranging to find the minimum n for a given tolerance: - -- Start with the inequality: E ≤ tolerance -- Substitute the expression for E: - 2π² / 3n² ≤ tolerance -- Rearrange to isolate n²: - n² ≥ 2π² / (3 * tolerance) -- Taking the square root: - n ≥ π * sqrt(2 / (3 * tolerance)) +Start with the inequality: E ≤ tolerance +Substitute the expression for E: +2π² / 3n² ≤ tolerance +Rearrange to isolate n²: +n² ≥ 2π² / (3 * tolerance) +Taking the square root: +n ≥ π * sqrt(2 / (3 * tolerance)) :param radius: The radius of the circle to approximate -:param tolerance: Maximum acceptable area error in percentage +:param baseTolerance: Maximum acceptable area error in percentage :param minSegments: The minimum number of segments to use :return: The number of segments needed @@ -538,8 +536,8 @@ intuitive approximation that can be useful when exact error bounds aren't requir Mathematical approach: 1. Linear scaling: segments = constant * radius -- Larger constant = more segments = better approximation -- Smaller constant = fewer segments = coarser approximation +Larger constant = more segments = better approximation +Smaller constant = fewer segments = coarser approximation :param radius: The radius of the circle to approximate :param constant: Multiplier that determines the density of segments diff --git a/src/core/geometry/qgscircle.h b/src/core/geometry/qgscircle.h index 429e16110aea..f9018237d81a 100644 --- a/src/core/geometry/qgscircle.h +++ b/src/core/geometry/qgscircle.h @@ -387,31 +387,31 @@ class CORE_EXPORT QgsCircle : public QgsEllipse private : /** - * Calculate the number of segments needed to approximate a circle within a given tolerance. - * - * This function uses the sagitta (geometric chord height) to determine the number of segments - * required to approximate a circle such that the maximum deviation between the circle and its - * polygonal approximation is less than the specified tolerance. - * - * Mathematical approach: - * 1. Using the sagitta formula: s = r(1 - cos(θ/2)) - * where s is the sagitta, r is the radius, and θ is the segment angle - * 2. Substituting tolerance for s: - * tolerance = radius(1 - cos(θ/2)) - * 3. Solving for θ: - * tolerance/radius = 1 - cos(θ/2) - * cos(θ/2) = 1 - tolerance/radius - * θ/2 = arccos(1 - tolerance/radius) - * θ = 2 * arccos(1 - tolerance/radius) - * 4. Number of segments = ceil(2π / θ) - * = ceil(π / arccos(1 - tolerance/radius)) - * - * \param radius The radius of the circle to approximate - * \param tolerance Maximum allowed deviation between the circle and its polygonal approximation - * \param minSegments Minimum number of segments to use, regardless of the calculated value - * \returns The number of segments needed - * \note This is a private helper method - */ + * Calculate the number of segments needed to approximate a circle within a given tolerance. + * + * This function uses the sagitta (geometric chord height) to determine the number of segments + * required to approximate a circle such that the maximum deviation between the circle and its + * polygonal approximation is less than the specified tolerance. + * + * Mathematical approach: + * 1. Using the sagitta formula: s = r(1 - cos(θ/2)) + * where s is the sagitta, r is the radius, and θ is the segment angle + * 2. Substituting tolerance for s: + * tolerance = radius(1 - cos(θ/2)) + * 3. Solving for θ: + * tolerance/radius = 1 - cos(θ/2) + * cos(θ/2) = 1 - tolerance/radius + * θ/2 = arccos(1 - tolerance/radius) + * θ = 2 * arccos(1 - tolerance/radius) + * 4. Number of segments = ceil(2π / θ) + * = ceil(π / arccos(1 - tolerance/radius)) + * + * \param radius The radius of the circle to approximate + * \param tolerance Maximum allowed deviation between the circle and its polygonal approximation + * \param minSegments Minimum number of segments to use, regardless of the calculated value + * \returns The number of segments needed + * \note This is a private helper method + */ static int calculateSegmentsStandard( double radius, double tolerance, int minSegments ) { if ( tolerance >= radius ) @@ -427,38 +427,38 @@ class CORE_EXPORT QgsCircle : public QgsEllipse } /** - * Calculate the number of segments with adaptive tolerance based on radius. - * - * This method extends calculateSegments() by using an adaptive tolerance that scales - * with the radius to maintain better visual quality. While calculateSegments() uses - * a fixed tolerance, this version adjusts the tolerance based on the radius size. - * - * Mathematical approach: - * 1. Compute adaptive tolerance that varies with radius: - * adaptive_tolerance = base_tolerance * sqrt(radius) / log10(radius + 1) - * - * - For small radii: tolerance decreases → more segments for better detail - * - For large radii: tolerance increases gradually → fewer segments needed - * - sqrt(radius) provides basic scaling - * - log10(radius + 1) dampens the scaling for large radii - * - * 2. Apply sagitta-based calculation: - * - * - Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) - * - Number of segments = ceil(2π/angle) - * - * This adaptation ensures: - * - * - Small circles get more segments for better visual quality - * - Large circles don't get excessive segments - * - Smooth transition between different scales - * - * \param radius The radius of the circle to approximate - * \param tolerance Base tolerance value that will be scaled - * \param minSegments Minimum number of segments to use - * \returns The number of segments needed - * \note This is a private helper method - */ + * Calculate the number of segments with adaptive tolerance based on radius. + * + * This method extends calculateSegments() by using an adaptive tolerance that scales + * with the radius to maintain better visual quality. While calculateSegments() uses + * a fixed tolerance, this version adjusts the tolerance based on the radius size. + * + * Mathematical approach: + * 1. Compute adaptive tolerance that varies with radius: + * adaptive_tolerance = base_tolerance * sqrt(radius) / log10(radius + 1) + * + * For small radii: tolerance decreases → more segments for better detail + * For large radii: tolerance increases gradually → fewer segments needed + * sqrt(radius) provides basic scaling + * log10(radius + 1) dampens the scaling for large radii + * + * 2. Apply sagitta-based calculation: + * + * Calculate angle = 2 * arccos(1 - adaptive_tolerance/radius) + * Number of segments = ceil(2π/angle) + * + * This adaptation ensures: + * + * Small circles get more segments for better visual quality + * Large circles don't get excessive segments + * Smooth transition between different scales + * + * \param radius The radius of the circle to approximate + * \param tolerance Base tolerance value that will be scaled + * \param minSegments Minimum number of segments to use + * \returns The number of segments needed + * \note This is a private helper method + */ static int calculateSegmentsAdaptive( double radius, double tolerance, int minSegments ) { // Compute adaptive tolerance that varies with radius @@ -476,48 +476,46 @@ class CORE_EXPORT QgsCircle : public QgsEllipse } /** - * Calculate the number of segments based on the maximum allowed area error. - * - * This function computes the minimum number of segments needed to approximate - * a circle with a regular polygon such that the relative area error between - * the polygonal approximation and the actual circle is less than the specified tolerance. - * - * Mathematical derivation: - * 1. Area ratio between a regular n-sided polygon and a circle: - * - * - Circle area: Ac = πr² - * - Regular polygon area: Ap = (nr²/2) * sin(2π/n) - * - Ratio = Ap / Ac = (n / 2π) * sin(2π/n) - * - * 2. For relative error E: - * E = |1 - Ap / Ac| = |1 - (n / 2π) * sin(2π/n)| - * - * 3. Using Taylor series approximation for sin(x) when x is small: - * sin(x) ≈ x - x³ / 6 - * With x = 2π / n: - * sin(2π / n) ≈ (2π / n) - (2π / n)³ / 6 - * - * 4. Substituting and simplifying: - * E ≈ |1 - (n / 2π) * ((2π / n) - (2π / n)³ / 6)| - * E ≈ |1 - (1 - (2π² / 3n²))| - * E ≈ 2π² / 3n² - * - * 5. Rearranging to find the minimum n for a given tolerance: - * - * - Start with the inequality: E ≤ tolerance - * - Substitute the expression for E: - * 2π² / 3n² ≤ tolerance - * - Rearrange to isolate n²: - * n² ≥ 2π² / (3 * tolerance) - * - Taking the square root: - * n ≥ π * sqrt(2 / (3 * tolerance)) - * - * \param radius The radius of the circle to approximate - * \param tolerance Maximum acceptable area error in percentage - * \param minSegments The minimum number of segments to use - * \returns The number of segments needed - * \note This is a private helper method - */ + * Calculate the number of segments based on the maximum allowed area error. + * + * This function computes the minimum number of segments needed to approximate + * a circle with a regular polygon such that the relative area error between + * the polygonal approximation and the actual circle is less than the specified tolerance. + * + * Mathematical derivation: + * 1. Area ratio between a regular n-sided polygon and a circle: + * Circle area: Ac = πr² + * Regular polygon area: Ap = (nr²/2) * sin(2π/n) + * Ratio = Ap / Ac = (n / 2π) * sin(2π/n) + * + * 2. For relative error E: + * E = |1 - Ap / Ac| = |1 - (n / 2π) * sin(2π/n)| + * + * 3. Using Taylor series approximation for sin(x) when x is small: + * sin(x) ≈ x - x³ / 6 + * With x = 2π / n: + * sin(2π / n) ≈ (2π / n) - (2π / n)³ / 6 + * + * 4. Substituting and simplifying: + * E ≈ |1 - (n / 2π) * ((2π / n) - (2π / n)³ / 6)| + * E ≈ |1 - (1 - (2π² / 3n²))| + * E ≈ 2π² / 3n² + * + * 5. Rearranging to find the minimum n for a given tolerance: + * Start with the inequality: E ≤ tolerance + * Substitute the expression for E: + * 2π² / 3n² ≤ tolerance + * Rearrange to isolate n²: + * n² ≥ 2π² / (3 * tolerance) + * Taking the square root: + * n ≥ π * sqrt(2 / (3 * tolerance)) + * + * \param radius The radius of the circle to approximate + * \param baseTolerance Maximum acceptable area error in percentage + * \param minSegments The minimum number of segments to use + * \returns The number of segments needed + * \note This is a private helper method + */ static int calculateSegmentsByAreaError( double radius, double baseTolerance, int minSegments ) { Q_UNUSED( radius ); @@ -534,25 +532,25 @@ class CORE_EXPORT QgsCircle : public QgsEllipse } /** - * Calculate the number of segments using a simple linear relationship with radius. - * - * This function implements the simplest approach to circle discretization by using - * a direct linear relationship between the radius and the number of segments. - * While not mathematically precise for error control, it provides a quick and - * intuitive approximation that can be useful when exact error bounds aren't required. - * - * Mathematical approach: - * 1. Linear scaling: segments = constant * radius - * - * - Larger constant = more segments = better approximation - * - Smaller constant = fewer segments = coarser approximation - * - * \param radius The radius of the circle to approximate - * \param constant Multiplier that determines the density of segments - * \param minSegments The minimum number of segments to use - * \returns The number of segments needed - * \note This is a private helper method - */ + * Calculate the number of segments using a simple linear relationship with radius. + * + * This function implements the simplest approach to circle discretization by using + * a direct linear relationship between the radius and the number of segments. + * While not mathematically precise for error control, it provides a quick and + * intuitive approximation that can be useful when exact error bounds aren't required. + * + * Mathematical approach: + * 1. Linear scaling: segments = constant * radius + * + * Larger constant = more segments = better approximation + * Smaller constant = fewer segments = coarser approximation + * + * \param radius The radius of the circle to approximate + * \param constant Multiplier that determines the density of segments + * \param minSegments The minimum number of segments to use + * \returns The number of segments needed + * \note This is a private helper method + */ static int calculateSegmentsByConstant( double radius, double constant, int minSegments ) { return std::max( minSegments, static_cast( std::ceil( constant * radius ) ) );