Skip to content
Permalink
Browse files

Fix circle by three tangents parrallel when two are parallels (#40702)

* add new method to solve QgsCircle::from3tangents where 2 tangents are parallels
  • Loading branch information
lbartoletti committed Dec 22, 2020
1 parent 0b312d9 commit 817744928cd27da587fb31d499f18592940e9e50
@@ -93,7 +93,9 @@ The azimuth is the angle between ``center`` and ``pt1``.

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 ) /HoldGIL/;
const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3,
double epsilon = 1E-8,
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.
@@ -106,6 +108,70 @@ The azimuth always takes the default value.
:param pt1_tg3: First point of the third tangent.
:param pt2_tg3: Second point of the third tangent.
:param epsilon: Value used to compare point.
:param pos: Point to determine which circle use in case of multi return.
If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangets are parallels. (since QGIS 3.18)

.. seealso:: :py:func:`from3TangentsMulti`

Example
-------

.. code-block:: python

# [(0 0), (5 0)] and [(5 5), (10 5)] are parallels
QgsCircle.from3Tangents(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5))
# <QgsCircle: Empty>
QgsCircle.from3Tangents(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(2, 0))
# <QgsCircle: Circle (Center: Point (1.46446609406726203 2.49999999999999911), Radius: 2.5, Azimuth: 0)>
QgsCircle.from3Tangents(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(3, 0))
# <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,
QgsPoint pos = QgsPoint() ) /HoldGIL/;
%Docstring
Returns an array of circle constructed by 3 tangents on the circle (aka inscribed circle of a triangle).

The vector can contain 0, 1 or 2 circles:

- 0: Impossible to construct a circle from 3 tangents (three parallel tangents)
- 1: The three tangents make a triangle or when two tangents are parallel there are two possible circles (see examples).
If pos is not an empty point, we use its coordinates to determine which circle will be returned.
More precisely the circle that will be returned will be the one whose center is on the same side as pos relative to the third tangent.
- 2: Returns both solutions when two tangents are parallel (this implies that pos is an empty point).

Z and m values are dropped for the center point.
The azimuth always takes the default value.

:param pt1_tg1: First point of the first tangent.
:param pt2_tg1: Second point of the first tangent.
:param pt1_tg2: First point of the second tangent.
:param pt2_tg2: Second point of the second tangent.
:param pt1_tg3: First point of the third tangent.
:param pt2_tg3: Second point of the third tangent.
:param epsilon: Value used to compare point.
:param pos: (optional) Point to determine which circle use in case of multi return.

.. seealso:: :py:func:`from3Tangents`

Example
-------

.. code-block:: python

# [(0 0), (5 0)] and [(5 5), (10 5)] are parallels
QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5))
# [<QgsCircle: Circle (Center: Point (8.53553390593273775 2.5), Radius: 2.5, Azimuth: 0)>, <QgsCircle: Circle (Center: Point (1.46446609406726203 2.49999999999999911), Radius: 2.5, Azimuth: 0)>]
QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(2, 0))
# [<QgsCircle: Circle (Center: Point (1.46446609406726203 2.49999999999999911), Radius: 2.5, Azimuth: 0)>]
QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(3, 0))
# [<QgsCircle: Circle (Center: Point (8.53553390593273775 2.5), Radius: 2.5, Azimuth: 0)>]
# [(0 0), (5 0)], [(5 5), (10 5)] and [(15 5), (20 5)] are parallels
QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(15, 5), QgsPoint(20, 5))
# []
%End

static QgsCircle fromExtent( const QgsPoint &pt1, const QgsPoint &pt2 ) /HoldGIL/;
@@ -34,6 +34,19 @@ QgsMapToolCircle3Tangents::QgsMapToolCircle3Tangents( QgsMapToolCapture *parentT
mToolName = tr( "Add circle from 3 tangents" );
}

static QgsPoint getFirstPointOnParallels( const QgsPoint p1_line1, const QgsPoint p2_line1, const QgsPoint pos_line1, const QgsPoint p1_line2, const QgsPoint p2_line2, const QgsPoint pos_line2, const QgsPoint p1_line3, const QgsPoint p2_line3 )
{
QgsPoint intersection;
bool isInter;

if ( ( !QgsGeometryUtils::segmentIntersection( p1_line1, p2_line1, p1_line2, p2_line2, intersection, isInter, true ) ) || ( !QgsGeometryUtils::segmentIntersection( p1_line1, p2_line1, p1_line3, p2_line3, intersection, isInter, true ) ) )
return pos_line1;
if ( !QgsGeometryUtils::segmentIntersection( p1_line2, p2_line2, p1_line3, p2_line3, intersection, isInter, true ) )
return pos_line2;

return QgsPoint();
}

void QgsMapToolCircle3Tangents::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
{
QgsPoint point = mapPoint( *e );
@@ -56,6 +69,7 @@ void QgsMapToolCircle3Tangents::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
{
if ( match.isValid() && ( mPoints.size() <= 2 * 2 ) )
{
mPosPoints.append( mapPoint( match.point() ) );
match.edgePoints( p1, p2 );
mPoints.append( mapPoint( p1 ) );
mPoints.append( mapPoint( p2 ) );
@@ -68,11 +82,13 @@ void QgsMapToolCircle3Tangents::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
match.edgePoints( p1, p2 );
mPoints.append( mapPoint( p1 ) );
mPoints.append( mapPoint( p2 ) );
mCircle = QgsCircle().from3Tangents( mPoints.at( 0 ), mPoints.at( 1 ), mPoints.at( 2 ), mPoints.at( 3 ), mPoints.at( 4 ), mPoints.at( 5 ) );
const QgsPoint pos = getFirstPointOnParallels( mPoints.at( 0 ), mPoints.at( 1 ), mPosPoints.at( 0 ), mPoints.at( 2 ), mPoints.at( 3 ), mPosPoints.at( 1 ), mPoints.at( 4 ), mPoints.at( 5 ) );
mCircle = QgsCircle().from3Tangents( mPoints.at( 0 ), mPoints.at( 1 ), mPoints.at( 2 ), mPoints.at( 3 ), mPoints.at( 4 ), mPoints.at( 5 ), 1E-8, pos );
if ( mCircle.isEmpty() )
{
QgisApp::instance()->messageBar()->pushMessage( tr( "Error" ), tr( "At least two segments are parallels" ), Qgis::Critical );
QgisApp::instance()->messageBar()->pushMessage( tr( "Error" ), tr( "The three segments are parallel" ), Qgis::Critical );
mPoints.clear();
mPosPoints.clear();
delete mTempRubberBand;
mTempRubberBand = nullptr;
}
@@ -104,7 +120,8 @@ void QgsMapToolCircle3Tangents::cadCanvasMoveEvent( QgsMapMouseEvent *e )
match.edgePoints( p1, p2 );
if ( mPoints.size() == 4 )
{
mCircle = QgsCircle().from3Tangents( mPoints.at( 0 ), mPoints.at( 1 ), mPoints.at( 2 ), mPoints.at( 3 ), QgsPoint( p1 ), QgsPoint( p2 ) );
const QgsPoint pos = getFirstPointOnParallels( mPoints.at( 0 ), mPoints.at( 1 ), mPosPoints.at( 0 ), mPoints.at( 2 ), mPoints.at( 3 ), mPosPoints.at( 1 ), QgsPoint( p1 ), QgsPoint( p2 ) );
mCircle = QgsCircle().from3Tangents( mPoints.at( 0 ), mPoints.at( 1 ), mPoints.at( 2 ), mPoints.at( 3 ), QgsPoint( p1 ), QgsPoint( p2 ), 1E-8, pos );
mTempRubberBand->setGeometry( mCircle.toLineString() );
mTempRubberBand->show();
}
@@ -123,3 +140,9 @@ void QgsMapToolCircle3Tangents::cadCanvasMoveEvent( QgsMapMouseEvent *e )
}

}

void QgsMapToolCircle3Tangents::clean( )
{
mPosPoints.clear();
QgsMapToolAddCircle::clean();
}
@@ -29,6 +29,10 @@ class QgsMapToolCircle3Tangents: public QgsMapToolAddCircle

void cadCanvasReleaseEvent( QgsMapMouseEvent *e ) override;
void cadCanvasMoveEvent( QgsMapMouseEvent *e ) override;
void clean() override;
private:
//! Snapped points on the segments. Useful to determine which circle to choose in case of there are two parallels
QVector<QgsPoint> mPosPoints;
};

#endif // QGSMAPTOOLCIRCLE3TANGENTS_H
@@ -188,19 +188,112 @@ QgsCircle QgsCircle::fromCenterPoint( const QgsPoint &center, const QgsPoint &pt
return QgsCircle( centerPt, centerPt.distance( pt1 ), azimuth );
}

QgsCircle 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 )
static QVector<QgsCircle> from2ParallelsLine( const QgsPoint &pt1_par1, const QgsPoint &pt2_par1, const QgsPoint &pt1_par2, const QgsPoint &pt2_par2, const QgsPoint &pt1_line1, const QgsPoint &pt2_line1, QgsPoint pos, double epsilon )
{
const double radius = QgsGeometryUtils::perpendicularSegment( pt1_par2, pt1_par1, pt2_par1 ).length() / 2.0;

bool isInter;
QgsPoint ptInter;
QVector<QgsCircle> circles;

QgsPoint ptInter_par1line1, ptInter_par2line1;
double angle1, angle2;
double x, y;
QgsGeometryUtils::angleBisector( pt1_par1.x(), pt1_par1.y(), pt2_par1.x(), pt2_par1.y(), pt1_line1.x(), pt1_line1.y(), pt2_line1.x(), pt2_line1.y(), x, y, angle1 );
ptInter_par1line1.setX( x );
ptInter_par1line1.setY( y );

QgsGeometryUtils::angleBisector( pt1_par2.x(), pt1_par2.y(), pt2_par2.x(), pt2_par2.y(), pt1_line1.x(), pt1_line1.y(), pt2_line1.x(), pt2_line1.y(), x, y, angle2 );
ptInter_par2line1.setX( x );
ptInter_par2line1.setY( y );

QgsPoint center;
QgsGeometryUtils::segmentIntersection( ptInter_par1line1, ptInter_par1line1.project( 1.0, angle1 ), ptInter_par2line1, ptInter_par2line1.project( 1.0, angle2 ), center, isInter, epsilon, true );
if ( isInter )
{
if ( !pos.isEmpty() )
{
if ( QgsGeometryUtils::leftOfLine( center, pt1_line1, pt2_line1 ) == QgsGeometryUtils::leftOfLine( pos, pt1_line1, pt2_line1 ) )
{
circles.append( QgsCircle( center, radius ) );
}
}
else
{
circles.append( QgsCircle( center, radius ) );
}
}

QgsGeometryUtils::segmentIntersection( ptInter_par1line1, ptInter_par1line1.project( 1.0, angle1 ), ptInter_par2line1, ptInter_par2line1.project( 1.0, angle2 + 90.0 ), center, isInter, epsilon, true );
if ( isInter )
{
if ( !pos.isEmpty() )
{
if ( QgsGeometryUtils::leftOfLine( center, pt1_line1, pt2_line1 ) == QgsGeometryUtils::leftOfLine( pos, pt1_line1, pt2_line1 ) )
{
circles.append( QgsCircle( center, radius ) );
}
}
else
{
circles.append( QgsCircle( center, radius ) );
}
}

QgsGeometryUtils::segmentIntersection( ptInter_par1line1, ptInter_par1line1.project( 1.0, angle1 + 90.0 ), ptInter_par2line1, ptInter_par2line1.project( 1.0, angle2 ), center, isInter, epsilon, true );
if ( isInter and not circles.contains( QgsCircle( center, radius ) ) )
{
if ( !pos.isEmpty() )
{
if ( QgsGeometryUtils::leftOfLine( center, pt1_line1, pt2_line1 ) == QgsGeometryUtils::leftOfLine( pos, pt1_line1, pt2_line1 ) )
{
circles.append( QgsCircle( center, radius ) );
}
}
else
{
circles.append( QgsCircle( center, radius ) );
}
}
QgsGeometryUtils::segmentIntersection( ptInter_par1line1, ptInter_par1line1.project( 1.0, angle1 + 90.0 ), ptInter_par2line1, ptInter_par2line1.project( 1.0, angle2 + 90.0 ), center, isInter, epsilon, true );
if ( isInter and not circles.contains( QgsCircle( center, radius ) ) )
{
if ( !pos.isEmpty() )
{
if ( QgsGeometryUtils::leftOfLine( center, pt1_line1, pt2_line1 ) == QgsGeometryUtils::leftOfLine( pos, pt1_line1, pt2_line1 ) )
{
circles.append( QgsCircle( center, radius ) );
}
}
else
{
circles.append( QgsCircle( center, radius ) );
}
}

return circles;
}

QVector<QgsCircle> 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, QgsPoint pos )
{
QgsPoint p1, p2, p3;
bool isIntersect = false;
QgsGeometryUtils::segmentIntersection( pt1_tg1, pt2_tg1, pt1_tg2, pt2_tg2, p1, isIntersect, epsilon );
if ( !isIntersect )
return QgsCircle();
QgsGeometryUtils::segmentIntersection( pt1_tg1, pt2_tg1, pt1_tg3, pt2_tg3, p2, isIntersect, epsilon );
if ( !isIntersect )
return QgsCircle();
QgsGeometryUtils::segmentIntersection( pt1_tg2, pt2_tg2, pt1_tg3, pt2_tg3, p3, isIntersect, epsilon );
if ( !isIntersect )
return QgsCircle();
bool isIntersect_tg1tg2 = false;
bool isIntersect_tg1tg3 = false;
bool isIntersect_tg2tg3 = false;
QgsGeometryUtils::segmentIntersection( pt1_tg1, pt2_tg1, pt1_tg2, pt2_tg2, p1, isIntersect_tg1tg2, epsilon );
QgsGeometryUtils::segmentIntersection( pt1_tg1, pt2_tg1, pt1_tg3, pt2_tg3, p2, isIntersect_tg1tg3, epsilon );
QgsGeometryUtils::segmentIntersection( pt1_tg2, pt2_tg2, pt1_tg3, pt2_tg3, p3, isIntersect_tg2tg3, epsilon );

QVector<QgsCircle> circles;
if ( !isIntersect_tg1tg2 && !isIntersect_tg2tg3 ) // three lines are parallels
return circles;

if ( !isIntersect_tg1tg2 )
return from2ParallelsLine( pt1_tg1, pt2_tg1, pt1_tg2, pt2_tg2, pt1_tg3, pt2_tg3, pos, epsilon );
else if ( !isIntersect_tg1tg3 )
return from2ParallelsLine( pt1_tg1, pt2_tg1, pt1_tg3, pt2_tg3, pt1_tg2, pt2_tg2, pos, epsilon );
else if ( !isIntersect_tg2tg3 )
return from2ParallelsLine( pt1_tg2, pt2_tg2, pt1_tg3, pt2_tg3, pt1_tg1, pt1_tg1, pos, epsilon );

if ( p1.is3D() )
{
@@ -217,7 +310,16 @@ QgsCircle QgsCircle::from3Tangents( const QgsPoint &pt1_tg1, const QgsPoint &pt2
p3.convertTo( QgsWkbTypes::dropZ( p3.wkbType() ) );
}

return QgsTriangle( p1, p2, p3 ).inscribedCircle();
circles.append( QgsTriangle( p1, p2, p3 ).inscribedCircle() );
return circles;
}

QgsCircle 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, QgsPoint pos )
{
const QVector<QgsCircle> circles = from3TangentsMulti( pt1_tg1, pt2_tg1, pt1_tg2, pt2_tg2, pt1_tg3, pt2_tg3, epsilon, pos );
if ( circles.length() != 1 )
return QgsCircle();
return circles.at( 0 );
}

QgsCircle QgsCircle::minimalCircleFrom3Points( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3, double epsilon )
@@ -111,10 +111,74 @@ class CORE_EXPORT QgsCircle : public QgsEllipse
* \param pt1_tg3 First point of the third tangent.
* \param pt2_tg3 Second point of the third tangent.
* \param epsilon Value used to compare point.
* \param pos Point to determine which circle use in case of multi return.
* If the solution is not unique and pos is an empty point, an empty circle is returned. -- This case happens only when two tangets are parallels. (since QGIS 3.18)
*
* \see from3TangentsMulti()
*
* ### Example
*
* \code{.py}
* # [(0 0), (5 0)] and [(5 5), (10 5)] are parallels
* QgsCircle.from3Tangents(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5))
* # <QgsCircle: Empty>
* QgsCircle.from3Tangents(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(2, 0))
* # <QgsCircle: Circle (Center: Point (1.46446609406726203 2.49999999999999911), Radius: 2.5, Azimuth: 0)>
* QgsCircle.from3Tangents(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(3, 0))
* # <QgsCircle: Circle (Center: Point (8.53553390593273775 2.5), Radius: 2.5, Azimuth: 0)>
* \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 ) SIP_HOLDGIL;
const QgsPoint &pt1_tg3, const QgsPoint &pt2_tg3,
double epsilon = 1E-8,
QgsPoint pos = QgsPoint() ) SIP_HOLDGIL;

/**
* Returns an array of circle constructed by 3 tangents on the circle (aka inscribed circle of a triangle).
*
* The vector can contain 0, 1 or 2 circles:
*
* - 0: Impossible to construct a circle from 3 tangents (three parallel tangents)
* - 1: The three tangents make a triangle or when two tangents are parallel there are two possible circles (see examples).
* If pos is not an empty point, we use its coordinates to determine which circle will be returned.
* More precisely the circle that will be returned will be the one whose center is on the same side as pos relative to the third tangent.
* - 2: Returns both solutions when two tangents are parallel (this implies that pos is an empty point).
*
* Z and m values are dropped for the center point.
* The azimuth always takes the default value.
* \param pt1_tg1 First point of the first tangent.
* \param pt2_tg1 Second point of the first tangent.
* \param pt1_tg2 First point of the second tangent.
* \param pt2_tg2 Second point of the second tangent.
* \param pt1_tg3 First point of the third tangent.
* \param pt2_tg3 Second point of the third tangent.
* \param epsilon Value used to compare point.
* \param pos (optional) Point to determine which circle use in case of multi return.
*
* \see from3Tangents()
*
* ### Example
*
* \code{.py}
*
* # [(0 0), (5 0)] and [(5 5), (10 5)] are parallels
* QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5))
* # [<QgsCircle: Circle (Center: Point (8.53553390593273775 2.5), Radius: 2.5, Azimuth: 0)>, <QgsCircle: Circle (Center: Point (1.46446609406726203 2.49999999999999911), Radius: 2.5, Azimuth: 0)>]
* QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(2, 0))
* # [<QgsCircle: Circle (Center: Point (1.46446609406726203 2.49999999999999911), Radius: 2.5, Azimuth: 0)>]
* QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(2.5, 0), QgsPoint(7.5, 5), pos=QgsPoint(3, 0))
* # [<QgsCircle: Circle (Center: Point (8.53553390593273775 2.5), Radius: 2.5, Azimuth: 0)>]
* # [(0 0), (5 0)], [(5 5), (10 5)] and [(15 5), (20 5)] are parallels
* QgsCircle.from3TangentsMulti(QgsPoint(0, 0), QgsPoint(5, 0), QgsPoint(5, 5), QgsPoint(10, 5), QgsPoint(15, 5), QgsPoint(20, 5))
* # []
* \endcode
*/
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,
QgsPoint pos = QgsPoint() ) SIP_HOLDGIL;

/**
* Constructs a circle by an extent (aka bounding box / QgsRectangle).

0 comments on commit 8177449

Please sign in to comment.
You can’t perform that action at this time.