Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(QgsCircle): Add segment calculation methods #60454

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions python/PyQt6/core/auto_additions/qgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions python/PyQt6/core/auto_additions/qgscircle.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
QgsCircle.from3TangentsMulti = staticmethod(QgsCircle.from3TangentsMulti)
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)
QgsCircle.__group__ = ['geometry']
except (NameError, AttributeError):
pass
190 changes: 173 additions & 17 deletions python/PyQt6/core/auto_generated/geometry/qgscircle.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@




class QgsCircle : QgsEllipse
{
%Docstring(signature="appended")
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -125,11 +122,7 @@ Example
# <QgsCircle: Circle (Center: Point (8.53553390593273775 2.5), Radius: 2.5, Azimuth: 0)>
%End

static QVector<QgsCircle> 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<QgsCircle> 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).

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -388,11 +377,178 @@ Coordinates are taken from quadrant North, East and South.
.. seealso:: :py:func:`asGml2`
%End

static int calculateSegments( double radius, double parameter, int minSegments, Qgis::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( "<QgsCircle: %1>" ).arg( sipCpp->toString() );
QString str
= QStringLiteral( "<QgsCircle: %1>" ).arg( sipCpp->toString() );
sipRes = PyUnicode_FromString( str.toUtf8().constData() );
%End

private :

static int calculateSegmentsStandard( double radius, double tolerance, int minSegments );
%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

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 baseTolerance: 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
};

/************************************************************************
Expand Down
8 changes: 8 additions & 0 deletions python/PyQt6/core/auto_generated/qgis.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions python/core/auto_additions/qgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
5 changes: 5 additions & 0 deletions python/core/auto_additions/qgscircle.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
QgsCircle.from3TangentsMulti = staticmethod(QgsCircle.from3TangentsMulti)
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)
QgsCircle.__group__ = ['geometry']
except (NameError, AttributeError):
pass
Loading