diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 0784b2631d86..5d6f8dd515f7 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -325,6 +325,9 @@ QgsWkbTypes.TIN = Qgis.WkbType.TIN QgsWkbTypes.TIN.is_monkey_patched = True QgsWkbTypes.TIN.__doc__ = "TIN \n.. versionadded:: 3.40" +QgsWkbTypes.NurbsCurve = Qgis.WkbType.NurbsCurve +QgsWkbTypes.NurbsCurve.is_monkey_patched = True +QgsWkbTypes.NurbsCurve.__doc__ = "NurbsCurve \n.. versionadded:: 4.0" QgsWkbTypes.NoGeometry = Qgis.WkbType.NoGeometry QgsWkbTypes.NoGeometry.is_monkey_patched = True QgsWkbTypes.NoGeometry.__doc__ = "No geometry" @@ -373,6 +376,9 @@ QgsWkbTypes.TINZ = Qgis.WkbType.TINZ QgsWkbTypes.TINZ.is_monkey_patched = True QgsWkbTypes.TINZ.__doc__ = "TINZ" +QgsWkbTypes.NurbsCurveZ = Qgis.WkbType.NurbsCurveZ +QgsWkbTypes.NurbsCurveZ.is_monkey_patched = True +QgsWkbTypes.NurbsCurveZ.__doc__ = "NurbsCurveZ \n.. versionadded:: 4.0" QgsWkbTypes.PointM = Qgis.WkbType.PointM QgsWkbTypes.PointM.is_monkey_patched = True QgsWkbTypes.PointM.__doc__ = "PointM" @@ -418,6 +424,9 @@ QgsWkbTypes.TINM = Qgis.WkbType.TINM QgsWkbTypes.TINM.is_monkey_patched = True QgsWkbTypes.TINM.__doc__ = "TINM" +QgsWkbTypes.NurbsCurveM = Qgis.WkbType.NurbsCurveM +QgsWkbTypes.NurbsCurveM.is_monkey_patched = True +QgsWkbTypes.NurbsCurveM.__doc__ = "NurbsCurveM \n.. versionadded:: 4.0" QgsWkbTypes.PointZM = Qgis.WkbType.PointZM QgsWkbTypes.PointZM.is_monkey_patched = True QgsWkbTypes.PointZM.__doc__ = "PointZM" @@ -463,6 +472,9 @@ QgsWkbTypes.TriangleZM = Qgis.WkbType.TriangleZM QgsWkbTypes.TriangleZM.is_monkey_patched = True QgsWkbTypes.TriangleZM.__doc__ = "TriangleZM" +QgsWkbTypes.NurbsCurveZM = Qgis.WkbType.NurbsCurveZM +QgsWkbTypes.NurbsCurveZM.is_monkey_patched = True +QgsWkbTypes.NurbsCurveZM.__doc__ = "NurbsCurveZM \n.. versionadded:: 4.0" QgsWkbTypes.Point25D = Qgis.WkbType.Point25D QgsWkbTypes.Point25D.is_monkey_patched = True QgsWkbTypes.Point25D.__doc__ = "Point25D" @@ -531,6 +543,10 @@ .. versionadded:: 3.40 +* ``NurbsCurve``: NurbsCurve + + .. versionadded:: 4.0 + * ``NoGeometry``: No geometry * ``PointZ``: PointZ * ``LineStringZ``: LineStringZ @@ -547,6 +563,10 @@ * ``MultiSurfaceZ``: MultiSurfaceZ * ``PolyhedralSurfaceZ``: PolyhedralSurfaceZ * ``TINZ``: TINZ +* ``NurbsCurveZ``: NurbsCurveZ + + .. versionadded:: 4.0 + * ``PointM``: PointM * ``LineStringM``: LineStringM * ``PolygonM``: PolygonM @@ -562,6 +582,10 @@ * ``MultiSurfaceM``: MultiSurfaceM * ``PolyhedralSurfaceM``: PolyhedralSurfaceM * ``TINM``: TINM +* ``NurbsCurveM``: NurbsCurveM + + .. versionadded:: 4.0 + * ``PointZM``: PointZM * ``LineStringZM``: LineStringZM * ``PolygonZM``: PolygonZM @@ -577,6 +601,10 @@ * ``PolyhedralSurfaceZM``: PolyhedralSurfaceM * ``TINZM``: TINZM * ``TriangleZM``: TriangleZM +* ``NurbsCurveZM``: NurbsCurveZM + + .. versionadded:: 4.0 + * ``Point25D``: Point25D * ``LineString25D``: LineString25D * ``Polygon25D``: Polygon25D @@ -5689,6 +5717,10 @@ QgsVertexId.VertexType.CurveVertex = Qgis.VertexType.Curve QgsVertexId.CurveVertex.is_monkey_patched = True QgsVertexId.CurveVertex.__doc__ = "An intermediate point on a segment defining the curvature of the segment" +QgsVertexId.ControlPointVertex = Qgis.VertexType.ControlPoint +QgsVertexId.VertexType.ControlPointVertex = Qgis.VertexType.ControlPoint +QgsVertexId.ControlPointVertex.is_monkey_patched = True +QgsVertexId.ControlPointVertex.__doc__ = "A NURBS control point (does not lie on the curve) \n.. versionadded:: 4.0" Qgis.VertexType.__doc__ = """Types of vertex. .. versionadded:: 3.22 @@ -5701,6 +5733,13 @@ Available as ``QgsVertexId.CurveVertex`` in older QGIS releases. +* ``ControlPoint``: A NURBS control point (does not lie on the curve) + + .. versionadded:: 4.0 + + + Available as ``QgsVertexId.ControlPointVertex`` in older QGIS releases. + """ # -- diff --git a/python/PyQt6/core/auto_additions/qgsgeometryutils.py b/python/PyQt6/core/auto_additions/qgsgeometryutils.py index 020abfb7adfd..69f26904425c 100644 --- a/python/PyQt6/core/auto_additions/qgsgeometryutils.py +++ b/python/PyQt6/core/auto_additions/qgsgeometryutils.py @@ -14,6 +14,7 @@ QgsGeometryUtils.projectPointOnSegment = staticmethod(QgsGeometryUtils.projectPointOnSegment) QgsGeometryUtils.leftOfLine = staticmethod(QgsGeometryUtils.leftOfLine) QgsGeometryUtils.interpolatePointOnArc = staticmethod(QgsGeometryUtils.interpolatePointOnArc) + QgsGeometryUtils.interpolatePointOnCubicBezier = staticmethod(QgsGeometryUtils.interpolatePointOnCubicBezier) QgsGeometryUtils.segmentMidPoint = staticmethod(QgsGeometryUtils.segmentMidPoint) QgsGeometryUtils.segmentMidPointFromCenter = staticmethod(QgsGeometryUtils.segmentMidPointFromCenter) QgsGeometryUtils.circleTangentDirection = staticmethod(QgsGeometryUtils.circleTangentDirection) diff --git a/python/PyQt6/core/auto_additions/qgsnurbscurve.py b/python/PyQt6/core/auto_additions/qgsnurbscurve.py new file mode 100644 index 000000000000..1f8dec946923 --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgsnurbscurve.py @@ -0,0 +1,6 @@ +# The following has been generated automatically from src/core/geometry/qgsnurbscurve.h +try: + QgsNurbsCurve.__overridden_methods__ = ['clone', 'isClosed', 'isClosed2D', 'curveToLine', 'draw', 'drawAsPolygon', 'endPoint', 'equals', 'indexOf', 'interpolatePoint', 'numPoints', 'pointAt', 'points', 'reversed', 'scroll', 'startPoint', 'sumUpArea', 'sumUpArea3D', 'xAt', 'yAt', 'zAt', 'mAt', 'asQPolygonF', 'addToPainterPath', 'curveSubstring', 'length', 'segmentLength', 'distanceBetweenVertices', 'snappedToGrid', 'simplifyByDistance', 'removeDuplicateNodes', 'vertexAngle', 'swapXy', 'transform', 'createEmptyWithSameType', 'closestSegment', 'boundingBox', 'boundingBox3D', 'moveVertex', 'insertVertex', 'wkbSize', 'asWkb', 'asWkt', 'asGml2', 'asGml3', 'asKml', 'dimension', 'isEmpty', 'clear', 'boundingBoxIntersects', 'centroid', 'addZValue', 'addMValue', 'dropZValue', 'dropMValue', 'deleteVertex', 'fromWkb', 'fromWkt', 'fuzzyEqual', 'fuzzyDistanceEqual', 'geometryType', 'hasCurvedSegments', 'partCount', 'toCurveType', 'vertexAt', 'vertexCount', 'vertexNumberFromVertexId', 'isValid', 'clearCache', 'compareToSameClass', 'calculateBoundingBox3D'] + QgsNurbsCurve.__group__ = ['geometry'] +except (NameError, AttributeError): + pass diff --git a/python/PyQt6/core/auto_additions/qgsnurbsutils.py b/python/PyQt6/core/auto_additions/qgsnurbsutils.py new file mode 100644 index 000000000000..bd4d514ea9e6 --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgsnurbsutils.py @@ -0,0 +1,7 @@ +# The following has been generated automatically from src/core/geometry/qgsnurbsutils.h +try: + QgsNurbsUtils.containsNurbsCurve = staticmethod(QgsNurbsUtils.containsNurbsCurve) + QgsNurbsUtils.findNurbsCurveForVertex = staticmethod(QgsNurbsUtils.findNurbsCurveForVertex) + QgsNurbsUtils.__group__ = ['geometry'] +except (NameError, AttributeError): + pass diff --git a/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in index c756a2b42c48..7c1a37ee44e9 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in @@ -46,6 +46,8 @@ Abstract base class for all geometries. sipType = sipType_QgsCircularString; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) sipType = sipType_QgsCompoundCurve; + else if ( qgsgeometry_cast( sipCpp ) != nullptr ) + sipType = sipType_QgsNurbsCurve; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) sipType = sipType_QgsTriangle; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in index c02b1fc16f7e..94a270dec90d 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in @@ -214,6 +214,30 @@ Any z or m values present in the points will also be linearly interpolated in the output. .. versionadded:: 3.4 +%End + + static QgsPoint interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t ) /HoldGIL/; +%Docstring +Evaluates a point on a cubic Bézier curve defined by four control +points. + +:param p0: start point (the curve passes through this point) +:param p1: first control point +:param p2: second control point +:param p3: end point (the curve passes through this point) +:param t: parameter value between 0 and 1 + +:return: the point on the Bézier curve at parameter ``t`` + +Any Z or M values present in the input points will also be interpolated. + +The cubic Bézier formula is: + +.. code-block:: text + + B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + +.. versionadded:: 4.0 %End static bool segmentMidPoint( const QgsPoint &p1, const QgsPoint &p2, QgsPoint &result /Out/, double radius, const QgsPoint &mousePos ) /HoldGIL/; diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in index e4a5a6f64ff6..b4309963895b 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils_base.sip.in @@ -84,6 +84,7 @@ segment) and the result is undefined. %End + static void perpendicularOffsetPointAlongSegment( double x1, double y1, double x2, double y2, double proportion, double offset, double *x /Out/, double *y /Out/ ); %Docstring Calculates a point a certain ``proportion`` of the way along the segment diff --git a/python/PyQt6/core/auto_generated/geometry/qgsnurbscurve.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsnurbscurve.sip.in new file mode 100644 index 000000000000..adc6de3e7df0 --- /dev/null +++ b/python/PyQt6/core/auto_generated/geometry/qgsnurbscurve.sip.in @@ -0,0 +1,365 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbscurve.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + + +class QgsNurbsCurve : QgsCurve +{ +%Docstring(signature="appended") +Represents a NURBS (Non-Uniform Rational B-Spline) curve geometry in +2D/3D. + +NURBS curves are a mathematical model commonly used in computer graphics +for representing curves. They are parametric curves defined by control +points, weights, knot vectors, and a degree. + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgsnurbscurve.h" +%End + public: + QgsNurbsCurve(); +%Docstring +Constructor for an empty NURBS curve geometry. +%End + + QgsNurbsCurve( const QVector &controlPoints, int degree, const QVector &knots, const QVector &weights ); +%Docstring +Constructs a NURBS curve from control points, degree, knot vector and +weights. + +:param controlPoints: control points defining the curve. The number of + control points must be strictly greater than + ``degree`` +:param degree: degree of the NURBS curve (must be >= 1, typically 1-3) +:param knots: knot vector (must have size = control points count + + degree + 1, values must be non-decreasing) +:param weights: weight vector for rational curves (same size as control + points) +%End + + virtual QgsNurbsCurve *clone() const /Factory/; + + + + SIP_PYOBJECT evaluate( double t ) const /TypeHint="QgsPoint"/; +%Docstring +Evaluates the NURBS curve at parameter t ∈ [0,1]. Uses the Cox-de Boor +algorithm for B-spline basis function evaluation. + +:param t: parameter value between 0 and 1 + +:return: point on the curve at parameter t + +:raises ValueError: if t is not in range [0, 1] +%End +%MethodCode + if ( a0 < 0.0 || a0 > 1.0 ) + { + PyErr_SetString( PyExc_ValueError, "Parameter t must be in range [0, 1]" ); + sipIsErr = 1; + } + else + { + sipRes = sipConvertFromType( new QgsPoint( sipCpp->evaluate( a0 ) ), sipType_QgsPoint, Py_None ); + } +%End + + bool isBezier() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve represents a Bézier curve. A Bézier curve +is a special case of NURBS with uniform weights and specific knot +vector. +%End + + bool isBSpline() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve represents a B-spline (non-rational +NURBS). +%End + + bool isRational() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve is rational (has non-uniform weights). +%End + + bool isPolyBezier() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve represents a poly-Bézier curve. A +poly-Bézier is a degree 3 NURBS with (n-1) divisible by 3, where n is +the number of control points. +%End + + virtual bool isClosed() const /HoldGIL/; + + virtual bool isClosed2D() const /HoldGIL/; + + + virtual QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/; + + virtual void draw( QPainter &p ) const; + + virtual void drawAsPolygon( QPainter &p ) const; + + virtual QgsPoint endPoint() const /HoldGIL/; + + virtual bool equals( const QgsCurve &other ) const; + + virtual int indexOf( const QgsPoint &point ) const; + + virtual QgsPoint *interpolatePoint( double distance ) const /Factory/; + + virtual int numPoints() const /HoldGIL/; + + virtual bool pointAt( int node, QgsPoint &point /Out/, Qgis::VertexType &type /Out/ ) const; + + virtual void points( QgsPointSequence &pts /Out/ ) const; + + virtual QgsCurve *reversed() const /Factory/; + + virtual void scroll( int firstVertexIndex ); + + virtual QgsPoint startPoint() const /HoldGIL/; + + virtual void sumUpArea( double &sum /Out/ ) const; + + virtual void sumUpArea3D( double &sum /Out/ ) const; + + virtual double xAt( int index ) const; + + virtual double yAt( int index ) const; + + virtual double zAt( int index ) const; + + virtual double mAt( int index ) const; + + + virtual QPolygonF asQPolygonF() const; + + + virtual void addToPainterPath( QPainterPath &path ) const; + + virtual QgsCurve *curveSubstring( double startDistance, double endDistance ) const /Factory/; + + virtual double length() const /HoldGIL/; + + virtual double segmentLength( QgsVertexId startVertex ) const; + + virtual double distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const; + + virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; + + virtual QgsAbstractGeometry *simplifyByDistance( double tolerance ) const /Factory/; + + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + + virtual double vertexAngle( QgsVertexId vertex ) const; + + virtual void swapXy(); + + virtual bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = 0 ); + + virtual QgsAbstractGeometry *createEmptyWithSameType() const /Factory/; + + virtual double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt /Out/, QgsVertexId &vertexAfter /Out/, int *leftOf /Out/ = 0, double epsilon = 4 * DBL_EPSILON ) const; + + virtual void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ); + + virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ); + + virtual QgsRectangle boundingBox() const; + + virtual QgsBox3D boundingBox3D() const; + + virtual bool moveVertex( QgsVertexId position, const QgsPoint &newPos ); + + virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex ); + + virtual int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QString asWkt( int precision = 17 ) const; + + virtual QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QString asKml( int precision = 17 ) const; + + virtual int dimension() const /HoldGIL/; + + virtual bool isEmpty() const /HoldGIL/; + + virtual void clear(); + + virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/; + + virtual bool boundingBoxIntersects( const QgsBox3D &box3d ) const /HoldGIL/; + + virtual QgsPoint centroid() const; + + + virtual bool addZValue( double zValue = 0 ); + + virtual bool addMValue( double mValue = 0 ); + + virtual bool dropZValue(); + + virtual bool dropMValue(); + + virtual bool deleteVertex( QgsVertexId position ); + + virtual bool fromWkb( QgsConstWkbPtr &wkb ); + + virtual bool fromWkt( const QString &wkt ); + + virtual bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + + virtual bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + + virtual QString geometryType() const /HoldGIL/; + + virtual bool hasCurvedSegments() const /HoldGIL/; + + virtual int partCount() const /HoldGIL/; + + virtual QgsCurve *toCurveType() const; + + virtual QgsPoint vertexAt( QgsVertexId id ) const; + + virtual int vertexCount( int part = 0, int ring = 0 ) const /HoldGIL/; + + virtual int vertexNumberFromVertexId( QgsVertexId id ) const; + + virtual bool isValid( QString &error /Out/, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const; + + + int degree() const /HoldGIL/; +%Docstring +Returns the degree of the NURBS curve. +%End + + void setDegree( int degree ); +%Docstring +Sets the degree of the NURBS curve. + +:param degree: curve degree (typically 1-3) +%End + + QVector controlPoints() const /HoldGIL/; +%Docstring +Returns the control points of the NURBS curve. +%End + + void setControlPoints( const QVector &points ); +%Docstring +Sets the control points of the NURBS curve. + +:param points: control points +%End + + QVector knots() const /HoldGIL/; +%Docstring +Returns the knot vector of the NURBS curve. +%End + + void setKnots( const QVector &knots ); +%Docstring +Sets the knot vector of the NURBS curve. + +:param knots: knot vector (must have size = control points count + + degree + 1, values must be non-decreasing) +%End + + QVector weights() const /HoldGIL/; +%Docstring +Returns the weight vector of the NURBS curve. +%End + + void setWeights( const QVector &weights ); +%Docstring +Sets the weight vector of the NURBS curve. + +:param weights: weight vector (same size as control points) +%End + + + double weight( int index ) const /HoldGIL/; +%Docstring +Returns the weight at the specified control point ``index``. + +:raises IndexError: if no control point with the specified index exists. + +.. versionadded:: 4.0 +%End +%MethodCode + const int count = sipCpp->controlPoints().size(); + if ( a0 < 0 || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + sipRes = sipCpp->weight( a0 ); + } +%End + + void setWeight( int index, double weight ); +%Docstring +Sets the ``weight`` at the specified control point ``index``. Weight +must be positive (> 0). + +:raises IndexError: if no control point with the specified index exists. + +:raises ValueError: if weight is not positive. + +.. versionadded:: 4.0 +%End +%MethodCode + const int count = sipCpp->controlPoints().size(); + if ( a0 < 0 || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else if ( a1 <= 0 ) + { + PyErr_SetString( PyExc_ValueError, "Weight must be positive (> 0)" ); + sipIsErr = 1; + } + else + { + sipCpp->setWeight( a0, a1 ); + } +%End + + + + protected: + virtual void clearCache() const; + + int compareToSameClass( const QgsAbstractGeometry *other ) const final; + virtual QgsBox3D calculateBoundingBox3D() const; + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbscurve.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/geometry/qgsnurbsutils.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsnurbsutils.sip.in new file mode 100644 index 000000000000..d720d79ddd22 --- /dev/null +++ b/python/PyQt6/core/auto_generated/geometry/qgsnurbsutils.sip.in @@ -0,0 +1,61 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbsutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsNurbsUtils +{ +%Docstring(signature="appended") +Utility functions for working with NURBS curves. + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgsnurbsutils.h" +%End + public: + + static bool containsNurbsCurve( const QgsAbstractGeometry *geom ); +%Docstring +Returns ``True`` if the ``geom`` contains a NURBS curve (recursively). +%End + + static const QgsNurbsCurve *extractNurbsCurve( const QgsAbstractGeometry *geom ); +%Docstring +Extracts the first NURBS curve found in the ``geom`` (recursively). +Returns ``None`` if no NURBS curve is found. +%End + + + static QgsNurbsCurve *findNurbsCurveForVertex( + QgsAbstractGeometry *geom, + const QgsVertexId &vid, + int &localIndex /Out/ ); +%Docstring +Finds the NURBS curve containing the vertex identified by ``vid``. + +:param geom: the geometry to search in +:param vid: the vertex identifier to search for + +:return: - the NURBS curve containing the vertex, or ``None`` if the + vertex is not part of a NURBS curve + - localIndex: the control point index within the found NURBS + curve +%End +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbsutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 27762cff41dc..967dc8828a70 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -192,6 +192,7 @@ The development version MultiSurface, PolyhedralSurface, TIN, + NurbsCurve, NoGeometry, PointZ, LineStringZ, @@ -208,6 +209,7 @@ The development version MultiSurfaceZ, PolyhedralSurfaceZ, TINZ, + NurbsCurveZ, PointM, LineStringM, PolygonM, @@ -223,6 +225,7 @@ The development version MultiSurfaceM, PolyhedralSurfaceM, TINM, + NurbsCurveM, PointZM, LineStringZM, PolygonZM, @@ -238,6 +241,7 @@ The development version PolyhedralSurfaceZM, TINZM, TriangleZM, + NurbsCurveZM, Point25D, LineString25D, Polygon25D, @@ -1782,6 +1786,7 @@ The development version { Segment, Curve, + ControlPoint, }; enum class MarkerShape /BaseType=IntEnum/ diff --git a/python/PyQt6/core/class_map.yaml b/python/PyQt6/core/class_map.yaml index ad694e6e365d..057919b22c21 100644 --- a/python/PyQt6/core/class_map.yaml +++ b/python/PyQt6/core/class_map.yaml @@ -1258,8 +1258,8 @@ QgsArrowInferSchemaOptions.geometryColumnName: src/core/qgsarrowiterator.h#L123 QgsArrowInferSchemaOptions.setGeometryColumnName: src/core/qgsarrowiterator.h#L115 QgsArrowInferSchemaOptions: src/core/qgsarrowiterator.h#L103 QgsArrowIterator.QgsArrowIterator: src/core/qgsarrowiterator.h#L332 -QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L364 -QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L371 +QgsArrowIterator.inferSchema(QgsFeatureSource*): src/core/qgsarrowiterator.h#L364 +QgsArrowIterator.inferSchema(QgsFeatureSource*,QgsArrowInferSchemaOptions): src/core/qgsarrowiterator.h#L371 QgsArrowIterator.nextFeatures: src/core/qgsarrowiterator.h#L357 QgsArrowIterator.setSchema: src/core/qgsarrowiterator.h#L344 QgsArrowIterator.toArrayStream: src/core/qgsarrowiterator.h#L347 diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 67976cafb91f..eaa37acfb2aa 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -362,6 +362,8 @@ %Include auto_generated/geometry/qgsmultipoint.sip %Include auto_generated/geometry/qgsmultipolygon.sip %Include auto_generated/geometry/qgsmultisurface.sip +%Include auto_generated/geometry/qgsnurbscurve.sip +%Include auto_generated/geometry/qgsnurbsutils.sip %Include auto_generated/geometry/qgsorientedbox3d.sip %Include auto_generated/geometry/qgspoint.sip %Include auto_generated/geometry/qgspolygon.sip diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index d383eeb5d5d7..4845c957c872 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -316,6 +316,9 @@ QgsWkbTypes.TIN = Qgis.WkbType.TIN QgsWkbTypes.TIN.is_monkey_patched = True QgsWkbTypes.TIN.__doc__ = "TIN \n.. versionadded:: 3.40" +QgsWkbTypes.NurbsCurve = Qgis.WkbType.NurbsCurve +QgsWkbTypes.NurbsCurve.is_monkey_patched = True +QgsWkbTypes.NurbsCurve.__doc__ = "NurbsCurve \n.. versionadded:: 4.0" QgsWkbTypes.NoGeometry = Qgis.WkbType.NoGeometry QgsWkbTypes.NoGeometry.is_monkey_patched = True QgsWkbTypes.NoGeometry.__doc__ = "No geometry" @@ -364,6 +367,9 @@ QgsWkbTypes.TINZ = Qgis.WkbType.TINZ QgsWkbTypes.TINZ.is_monkey_patched = True QgsWkbTypes.TINZ.__doc__ = "TINZ" +QgsWkbTypes.NurbsCurveZ = Qgis.WkbType.NurbsCurveZ +QgsWkbTypes.NurbsCurveZ.is_monkey_patched = True +QgsWkbTypes.NurbsCurveZ.__doc__ = "NurbsCurveZ \n.. versionadded:: 4.0" QgsWkbTypes.PointM = Qgis.WkbType.PointM QgsWkbTypes.PointM.is_monkey_patched = True QgsWkbTypes.PointM.__doc__ = "PointM" @@ -409,6 +415,9 @@ QgsWkbTypes.TINM = Qgis.WkbType.TINM QgsWkbTypes.TINM.is_monkey_patched = True QgsWkbTypes.TINM.__doc__ = "TINM" +QgsWkbTypes.NurbsCurveM = Qgis.WkbType.NurbsCurveM +QgsWkbTypes.NurbsCurveM.is_monkey_patched = True +QgsWkbTypes.NurbsCurveM.__doc__ = "NurbsCurveM \n.. versionadded:: 4.0" QgsWkbTypes.PointZM = Qgis.WkbType.PointZM QgsWkbTypes.PointZM.is_monkey_patched = True QgsWkbTypes.PointZM.__doc__ = "PointZM" @@ -454,6 +463,9 @@ QgsWkbTypes.TriangleZM = Qgis.WkbType.TriangleZM QgsWkbTypes.TriangleZM.is_monkey_patched = True QgsWkbTypes.TriangleZM.__doc__ = "TriangleZM" +QgsWkbTypes.NurbsCurveZM = Qgis.WkbType.NurbsCurveZM +QgsWkbTypes.NurbsCurveZM.is_monkey_patched = True +QgsWkbTypes.NurbsCurveZM.__doc__ = "NurbsCurveZM \n.. versionadded:: 4.0" QgsWkbTypes.Point25D = Qgis.WkbType.Point25D QgsWkbTypes.Point25D.is_monkey_patched = True QgsWkbTypes.Point25D.__doc__ = "Point25D" @@ -522,6 +534,10 @@ .. versionadded:: 3.40 +* ``NurbsCurve``: NurbsCurve + + .. versionadded:: 4.0 + * ``NoGeometry``: No geometry * ``PointZ``: PointZ * ``LineStringZ``: LineStringZ @@ -538,6 +554,10 @@ * ``MultiSurfaceZ``: MultiSurfaceZ * ``PolyhedralSurfaceZ``: PolyhedralSurfaceZ * ``TINZ``: TINZ +* ``NurbsCurveZ``: NurbsCurveZ + + .. versionadded:: 4.0 + * ``PointM``: PointM * ``LineStringM``: LineStringM * ``PolygonM``: PolygonM @@ -553,6 +573,10 @@ * ``MultiSurfaceM``: MultiSurfaceM * ``PolyhedralSurfaceM``: PolyhedralSurfaceM * ``TINM``: TINM +* ``NurbsCurveM``: NurbsCurveM + + .. versionadded:: 4.0 + * ``PointZM``: PointZM * ``LineStringZM``: LineStringZM * ``PolygonZM``: PolygonZM @@ -568,6 +592,10 @@ * ``PolyhedralSurfaceZM``: PolyhedralSurfaceM * ``TINZM``: TINZM * ``TriangleZM``: TriangleZM +* ``NurbsCurveZM``: NurbsCurveZM + + .. versionadded:: 4.0 + * ``Point25D``: Point25D * ``LineString25D``: LineString25D * ``Polygon25D``: Polygon25D @@ -5632,6 +5660,10 @@ QgsVertexId.VertexType.CurveVertex = Qgis.VertexType.Curve QgsVertexId.CurveVertex.is_monkey_patched = True QgsVertexId.CurveVertex.__doc__ = "An intermediate point on a segment defining the curvature of the segment" +QgsVertexId.ControlPointVertex = Qgis.VertexType.ControlPoint +QgsVertexId.VertexType.ControlPointVertex = Qgis.VertexType.ControlPoint +QgsVertexId.ControlPointVertex.is_monkey_patched = True +QgsVertexId.ControlPointVertex.__doc__ = "A NURBS control point (does not lie on the curve) \n.. versionadded:: 4.0" Qgis.VertexType.__doc__ = """Types of vertex. .. versionadded:: 3.22 @@ -5644,6 +5676,13 @@ Available as ``QgsVertexId.CurveVertex`` in older QGIS releases. +* ``ControlPoint``: A NURBS control point (does not lie on the curve) + + .. versionadded:: 4.0 + + + Available as ``QgsVertexId.ControlPointVertex`` in older QGIS releases. + """ # -- diff --git a/python/core/auto_additions/qgsgeometryutils.py b/python/core/auto_additions/qgsgeometryutils.py index 020abfb7adfd..69f26904425c 100644 --- a/python/core/auto_additions/qgsgeometryutils.py +++ b/python/core/auto_additions/qgsgeometryutils.py @@ -14,6 +14,7 @@ QgsGeometryUtils.projectPointOnSegment = staticmethod(QgsGeometryUtils.projectPointOnSegment) QgsGeometryUtils.leftOfLine = staticmethod(QgsGeometryUtils.leftOfLine) QgsGeometryUtils.interpolatePointOnArc = staticmethod(QgsGeometryUtils.interpolatePointOnArc) + QgsGeometryUtils.interpolatePointOnCubicBezier = staticmethod(QgsGeometryUtils.interpolatePointOnCubicBezier) QgsGeometryUtils.segmentMidPoint = staticmethod(QgsGeometryUtils.segmentMidPoint) QgsGeometryUtils.segmentMidPointFromCenter = staticmethod(QgsGeometryUtils.segmentMidPointFromCenter) QgsGeometryUtils.circleTangentDirection = staticmethod(QgsGeometryUtils.circleTangentDirection) diff --git a/python/core/auto_additions/qgsnurbscurve.py b/python/core/auto_additions/qgsnurbscurve.py new file mode 100644 index 000000000000..1f8dec946923 --- /dev/null +++ b/python/core/auto_additions/qgsnurbscurve.py @@ -0,0 +1,6 @@ +# The following has been generated automatically from src/core/geometry/qgsnurbscurve.h +try: + QgsNurbsCurve.__overridden_methods__ = ['clone', 'isClosed', 'isClosed2D', 'curveToLine', 'draw', 'drawAsPolygon', 'endPoint', 'equals', 'indexOf', 'interpolatePoint', 'numPoints', 'pointAt', 'points', 'reversed', 'scroll', 'startPoint', 'sumUpArea', 'sumUpArea3D', 'xAt', 'yAt', 'zAt', 'mAt', 'asQPolygonF', 'addToPainterPath', 'curveSubstring', 'length', 'segmentLength', 'distanceBetweenVertices', 'snappedToGrid', 'simplifyByDistance', 'removeDuplicateNodes', 'vertexAngle', 'swapXy', 'transform', 'createEmptyWithSameType', 'closestSegment', 'boundingBox', 'boundingBox3D', 'moveVertex', 'insertVertex', 'wkbSize', 'asWkb', 'asWkt', 'asGml2', 'asGml3', 'asKml', 'dimension', 'isEmpty', 'clear', 'boundingBoxIntersects', 'centroid', 'addZValue', 'addMValue', 'dropZValue', 'dropMValue', 'deleteVertex', 'fromWkb', 'fromWkt', 'fuzzyEqual', 'fuzzyDistanceEqual', 'geometryType', 'hasCurvedSegments', 'partCount', 'toCurveType', 'vertexAt', 'vertexCount', 'vertexNumberFromVertexId', 'isValid', 'clearCache', 'compareToSameClass', 'calculateBoundingBox3D'] + QgsNurbsCurve.__group__ = ['geometry'] +except (NameError, AttributeError): + pass diff --git a/python/core/auto_additions/qgsnurbsutils.py b/python/core/auto_additions/qgsnurbsutils.py new file mode 100644 index 000000000000..bd4d514ea9e6 --- /dev/null +++ b/python/core/auto_additions/qgsnurbsutils.py @@ -0,0 +1,7 @@ +# The following has been generated automatically from src/core/geometry/qgsnurbsutils.h +try: + QgsNurbsUtils.containsNurbsCurve = staticmethod(QgsNurbsUtils.containsNurbsCurve) + QgsNurbsUtils.findNurbsCurveForVertex = staticmethod(QgsNurbsUtils.findNurbsCurveForVertex) + QgsNurbsUtils.__group__ = ['geometry'] +except (NameError, AttributeError): + pass diff --git a/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in b/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in index 542cb131c411..4f70ea512f05 100644 --- a/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in @@ -46,6 +46,8 @@ Abstract base class for all geometries. sipType = sipType_QgsCircularString; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) sipType = sipType_QgsCompoundCurve; + else if ( qgsgeometry_cast( sipCpp ) != nullptr ) + sipType = sipType_QgsNurbsCurve; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) sipType = sipType_QgsTriangle; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) diff --git a/python/core/auto_generated/geometry/qgsgeometryutils.sip.in b/python/core/auto_generated/geometry/qgsgeometryutils.sip.in index 2a04b49d5cc6..b1840a2ad739 100644 --- a/python/core/auto_generated/geometry/qgsgeometryutils.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometryutils.sip.in @@ -214,6 +214,30 @@ Any z or m values present in the points will also be linearly interpolated in the output. .. versionadded:: 3.4 +%End + + static QgsPoint interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t ) /HoldGIL/; +%Docstring +Evaluates a point on a cubic Bézier curve defined by four control +points. + +:param p0: start point (the curve passes through this point) +:param p1: first control point +:param p2: second control point +:param p3: end point (the curve passes through this point) +:param t: parameter value between 0 and 1 + +:return: the point on the Bézier curve at parameter ``t`` + +Any Z or M values present in the input points will also be interpolated. + +The cubic Bézier formula is: + +.. code-block:: text + + B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + +.. versionadded:: 4.0 %End static bool segmentMidPoint( const QgsPoint &p1, const QgsPoint &p2, QgsPoint &result /Out/, double radius, const QgsPoint &mousePos ) /HoldGIL/; diff --git a/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in b/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in index e4a5a6f64ff6..b4309963895b 100644 --- a/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometryutils_base.sip.in @@ -84,6 +84,7 @@ segment) and the result is undefined. %End + static void perpendicularOffsetPointAlongSegment( double x1, double y1, double x2, double y2, double proportion, double offset, double *x /Out/, double *y /Out/ ); %Docstring Calculates a point a certain ``proportion`` of the way along the segment diff --git a/python/core/auto_generated/geometry/qgsnurbscurve.sip.in b/python/core/auto_generated/geometry/qgsnurbscurve.sip.in new file mode 100644 index 000000000000..0f5afcefeda6 --- /dev/null +++ b/python/core/auto_generated/geometry/qgsnurbscurve.sip.in @@ -0,0 +1,365 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbscurve.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + + +class QgsNurbsCurve : QgsCurve +{ +%Docstring(signature="appended") +Represents a NURBS (Non-Uniform Rational B-Spline) curve geometry in +2D/3D. + +NURBS curves are a mathematical model commonly used in computer graphics +for representing curves. They are parametric curves defined by control +points, weights, knot vectors, and a degree. + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgsnurbscurve.h" +%End + public: + QgsNurbsCurve(); +%Docstring +Constructor for an empty NURBS curve geometry. +%End + + QgsNurbsCurve( const QVector &controlPoints, int degree, const QVector &knots, const QVector &weights ); +%Docstring +Constructs a NURBS curve from control points, degree, knot vector and +weights. + +:param controlPoints: control points defining the curve. The number of + control points must be strictly greater than + ``degree`` +:param degree: degree of the NURBS curve (must be >= 1, typically 1-3) +:param knots: knot vector (must have size = control points count + + degree + 1, values must be non-decreasing) +:param weights: weight vector for rational curves (same size as control + points) +%End + + virtual QgsNurbsCurve *clone() const /Factory/; + + + + SIP_PYOBJECT evaluate( double t ) const /TypeHint="QgsPoint"/; +%Docstring +Evaluates the NURBS curve at parameter t ∈ [0,1]. Uses the Cox-de Boor +algorithm for B-spline basis function evaluation. + +:param t: parameter value between 0 and 1 + +:return: point on the curve at parameter t + +:raises ValueError: if t is not in range [0, 1] +%End +%MethodCode + if ( a0 < 0.0 || a0 > 1.0 ) + { + PyErr_SetString( PyExc_ValueError, "Parameter t must be in range [0, 1]" ); + sipIsErr = 1; + } + else + { + sipRes = sipConvertFromType( new QgsPoint( sipCpp->evaluate( a0 ) ), sipType_QgsPoint, Py_None ); + } +%End + + bool isBezier() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve represents a Bézier curve. A Bézier curve +is a special case of NURBS with uniform weights and specific knot +vector. +%End + + bool isBSpline() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve represents a B-spline (non-rational +NURBS). +%End + + bool isRational() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve is rational (has non-uniform weights). +%End + + bool isPolyBezier() const /HoldGIL/; +%Docstring +Returns ``True`` if this curve represents a poly-Bézier curve. A +poly-Bézier is a degree 3 NURBS with (n-1) divisible by 3, where n is +the number of control points. +%End + + virtual bool isClosed() const /HoldGIL/; + + virtual bool isClosed2D() const /HoldGIL/; + + + virtual QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/; + + virtual void draw( QPainter &p ) const; + + virtual void drawAsPolygon( QPainter &p ) const; + + virtual QgsPoint endPoint() const /HoldGIL/; + + virtual bool equals( const QgsCurve &other ) const; + + virtual int indexOf( const QgsPoint &point ) const; + + virtual QgsPoint *interpolatePoint( double distance ) const /Factory/; + + virtual int numPoints() const /HoldGIL/; + + virtual bool pointAt( int node, QgsPoint &point /Out/, Qgis::VertexType &type /Out/ ) const; + + virtual void points( QgsPointSequence &pts /Out/ ) const; + + virtual QgsCurve *reversed() const /Factory/; + + virtual void scroll( int firstVertexIndex ); + + virtual QgsPoint startPoint() const /HoldGIL/; + + virtual void sumUpArea( double &sum /Out/ ) const; + + virtual void sumUpArea3D( double &sum /Out/ ) const; + + virtual double xAt( int index ) const; + + virtual double yAt( int index ) const; + + virtual double zAt( int index ) const; + + virtual double mAt( int index ) const; + + + virtual QPolygonF asQPolygonF() const; + + + virtual void addToPainterPath( QPainterPath &path ) const; + + virtual QgsCurve *curveSubstring( double startDistance, double endDistance ) const /Factory/; + + virtual double length() const /HoldGIL/; + + virtual double segmentLength( QgsVertexId startVertex ) const; + + virtual double distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const; + + virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; + + virtual QgsAbstractGeometry *simplifyByDistance( double tolerance ) const /Factory/; + + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + + virtual double vertexAngle( QgsVertexId vertex ) const; + + virtual void swapXy(); + + virtual bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = 0 ); + + virtual QgsAbstractGeometry *createEmptyWithSameType() const /Factory/; + + virtual double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt /Out/, QgsVertexId &vertexAfter /Out/, int *leftOf /Out/ = 0, double epsilon = 4 * DBL_EPSILON ) const; + + virtual void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ) throw( QgsCsException ); + + virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ); + + virtual QgsRectangle boundingBox() const; + + virtual QgsBox3D boundingBox3D() const; + + virtual bool moveVertex( QgsVertexId position, const QgsPoint &newPos ); + + virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex ); + + virtual int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QString asWkt( int precision = 17 ) const; + + virtual QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QString asKml( int precision = 17 ) const; + + virtual int dimension() const /HoldGIL/; + + virtual bool isEmpty() const /HoldGIL/; + + virtual void clear(); + + virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/; + + virtual bool boundingBoxIntersects( const QgsBox3D &box3d ) const /HoldGIL/; + + virtual QgsPoint centroid() const; + + + virtual bool addZValue( double zValue = 0 ); + + virtual bool addMValue( double mValue = 0 ); + + virtual bool dropZValue(); + + virtual bool dropMValue(); + + virtual bool deleteVertex( QgsVertexId position ); + + virtual bool fromWkb( QgsConstWkbPtr &wkb ); + + virtual bool fromWkt( const QString &wkt ); + + virtual bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + + virtual bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + + virtual QString geometryType() const /HoldGIL/; + + virtual bool hasCurvedSegments() const /HoldGIL/; + + virtual int partCount() const /HoldGIL/; + + virtual QgsCurve *toCurveType() const; + + virtual QgsPoint vertexAt( QgsVertexId id ) const; + + virtual int vertexCount( int part = 0, int ring = 0 ) const /HoldGIL/; + + virtual int vertexNumberFromVertexId( QgsVertexId id ) const; + + virtual bool isValid( QString &error /Out/, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const; + + + int degree() const /HoldGIL/; +%Docstring +Returns the degree of the NURBS curve. +%End + + void setDegree( int degree ); +%Docstring +Sets the degree of the NURBS curve. + +:param degree: curve degree (typically 1-3) +%End + + QVector controlPoints() const /HoldGIL/; +%Docstring +Returns the control points of the NURBS curve. +%End + + void setControlPoints( const QVector &points ); +%Docstring +Sets the control points of the NURBS curve. + +:param points: control points +%End + + QVector knots() const /HoldGIL/; +%Docstring +Returns the knot vector of the NURBS curve. +%End + + void setKnots( const QVector &knots ); +%Docstring +Sets the knot vector of the NURBS curve. + +:param knots: knot vector (must have size = control points count + + degree + 1, values must be non-decreasing) +%End + + QVector weights() const /HoldGIL/; +%Docstring +Returns the weight vector of the NURBS curve. +%End + + void setWeights( const QVector &weights ); +%Docstring +Sets the weight vector of the NURBS curve. + +:param weights: weight vector (same size as control points) +%End + + + double weight( int index ) const /HoldGIL/; +%Docstring +Returns the weight at the specified control point ``index``. + +:raises IndexError: if no control point with the specified index exists. + +.. versionadded:: 4.0 +%End +%MethodCode + const int count = sipCpp->controlPoints().size(); + if ( a0 < 0 || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + sipRes = sipCpp->weight( a0 ); + } +%End + + void setWeight( int index, double weight ); +%Docstring +Sets the ``weight`` at the specified control point ``index``. Weight +must be positive (> 0). + +:raises IndexError: if no control point with the specified index exists. + +:raises ValueError: if weight is not positive. + +.. versionadded:: 4.0 +%End +%MethodCode + const int count = sipCpp->controlPoints().size(); + if ( a0 < 0 || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else if ( a1 <= 0 ) + { + PyErr_SetString( PyExc_ValueError, "Weight must be positive (> 0)" ); + sipIsErr = 1; + } + else + { + sipCpp->setWeight( a0, a1 ); + } +%End + + + + protected: + virtual void clearCache() const; + + int compareToSameClass( const QgsAbstractGeometry *other ) const final; + virtual QgsBox3D calculateBoundingBox3D() const; + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbscurve.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/auto_generated/geometry/qgsnurbsutils.sip.in b/python/core/auto_generated/geometry/qgsnurbsutils.sip.in new file mode 100644 index 000000000000..d720d79ddd22 --- /dev/null +++ b/python/core/auto_generated/geometry/qgsnurbsutils.sip.in @@ -0,0 +1,61 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbsutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsNurbsUtils +{ +%Docstring(signature="appended") +Utility functions for working with NURBS curves. + +.. versionadded:: 4.0 +%End + +%TypeHeaderCode +#include "qgsnurbsutils.h" +%End + public: + + static bool containsNurbsCurve( const QgsAbstractGeometry *geom ); +%Docstring +Returns ``True`` if the ``geom`` contains a NURBS curve (recursively). +%End + + static const QgsNurbsCurve *extractNurbsCurve( const QgsAbstractGeometry *geom ); +%Docstring +Extracts the first NURBS curve found in the ``geom`` (recursively). +Returns ``None`` if no NURBS curve is found. +%End + + + static QgsNurbsCurve *findNurbsCurveForVertex( + QgsAbstractGeometry *geom, + const QgsVertexId &vid, + int &localIndex /Out/ ); +%Docstring +Finds the NURBS curve containing the vertex identified by ``vid``. + +:param geom: the geometry to search in +:param vid: the vertex identifier to search for + +:return: - the NURBS curve containing the vertex, or ``None`` if the + vertex is not part of a NURBS curve + - localIndex: the control point index within the found NURBS + curve +%End +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgsnurbsutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 6ffe62f153de..319f72139a5f 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -192,6 +192,7 @@ The development version MultiSurface, PolyhedralSurface, TIN, + NurbsCurve, NoGeometry, PointZ, LineStringZ, @@ -208,6 +209,7 @@ The development version MultiSurfaceZ, PolyhedralSurfaceZ, TINZ, + NurbsCurveZ, PointM, LineStringM, PolygonM, @@ -223,6 +225,7 @@ The development version MultiSurfaceM, PolyhedralSurfaceM, TINM, + NurbsCurveM, PointZM, LineStringZM, PolygonZM, @@ -238,6 +241,7 @@ The development version PolyhedralSurfaceZM, TINZM, TriangleZM, + NurbsCurveZM, Point25D, LineString25D, Polygon25D, @@ -1782,6 +1786,7 @@ The development version { Segment, Curve, + ControlPoint, }; enum class MarkerShape diff --git a/python/core/class_map.yaml b/python/core/class_map.yaml index cb34c1700680..a14ede45703a 100644 --- a/python/core/class_map.yaml +++ b/python/core/class_map.yaml @@ -1258,8 +1258,8 @@ QgsArrowInferSchemaOptions.geometryColumnName: src/core/qgsarrowiterator.h#L123 QgsArrowInferSchemaOptions.setGeometryColumnName: src/core/qgsarrowiterator.h#L115 QgsArrowInferSchemaOptions: src/core/qgsarrowiterator.h#L103 QgsArrowIterator.QgsArrowIterator: src/core/qgsarrowiterator.h#L332 -QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L364 -QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L371 +QgsArrowIterator.inferSchema(QgsFeatureSource*): src/core/qgsarrowiterator.h#L364 +QgsArrowIterator.inferSchema(QgsFeatureSource*,QgsArrowInferSchemaOptions): src/core/qgsarrowiterator.h#L371 QgsArrowIterator.nextFeatures: src/core/qgsarrowiterator.h#L357 QgsArrowIterator.setSchema: src/core/qgsarrowiterator.h#L344 QgsArrowIterator.toArrayStream: src/core/qgsarrowiterator.h#L347 diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 67976cafb91f..eaa37acfb2aa 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -362,6 +362,8 @@ %Include auto_generated/geometry/qgsmultipoint.sip %Include auto_generated/geometry/qgsmultipolygon.sip %Include auto_generated/geometry/qgsmultisurface.sip +%Include auto_generated/geometry/qgsnurbscurve.sip +%Include auto_generated/geometry/qgsnurbsutils.sip %Include auto_generated/geometry/qgsorientedbox3d.sip %Include auto_generated/geometry/qgspoint.sip %Include auto_generated/geometry/qgspolygon.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 13b99f1ec9ea..d34a89cc97c8 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -943,6 +943,8 @@ set(QGIS_CORE_SRCS geometry/qgsmultipoint.cpp geometry/qgsmultipolygon.cpp geometry/qgsmultisurface.cpp + geometry/qgsnurbscurve.cpp + geometry/qgsnurbsutils.cpp geometry/qgsorientedbox3d.cpp geometry/qgspoint.cpp geometry/qgspolygon.cpp @@ -1545,6 +1547,8 @@ set(QGIS_CORE_HDRS geometry/qgsmultipoint.h geometry/qgsmultipolygon.h geometry/qgsmultisurface.h + geometry/qgsnurbscurve.h + geometry/qgsnurbsutils.h geometry/qgsorientedbox3d.h geometry/qgspoint.h geometry/qgspolygon.h diff --git a/src/core/geometry/qgsabstractgeometry.h b/src/core/geometry/qgsabstractgeometry.h index 3fbc740cacd7..f12e7c718670 100644 --- a/src/core/geometry/qgsabstractgeometry.h +++ b/src/core/geometry/qgsabstractgeometry.h @@ -90,6 +90,8 @@ class CORE_EXPORT QgsAbstractGeometry sipType = sipType_QgsCircularString; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) sipType = sipType_QgsCompoundCurve; + else if ( qgsgeometry_cast( sipCpp ) != nullptr ) + sipType = sipType_QgsNurbsCurve; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) sipType = sipType_QgsTriangle; else if ( qgsgeometry_cast( sipCpp ) != nullptr ) diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index f24f5074983a..3d4f03f2a1ef 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -37,6 +37,7 @@ email : morb at ozemail dot com dot au #include "qgsmultilinestring.h" #include "qgsmultipoint.h" #include "qgsmultipolygon.h" +#include "qgsnurbsutils.h" #include "qgspoint.h" #include "qgspointxy.h" #include "qgspolygon.h" diff --git a/src/core/geometry/qgsgeometryfactory.cpp b/src/core/geometry/qgsgeometryfactory.cpp index 7b8957f397df..dc59cceb1f25 100644 --- a/src/core/geometry/qgsgeometryfactory.cpp +++ b/src/core/geometry/qgsgeometryfactory.cpp @@ -27,6 +27,7 @@ #include "qgsmultipoint.h" #include "qgsmultipolygon.h" #include "qgsmultisurface.h" +#include "qgsnurbscurve.h" #include "qgspoint.h" #include "qgspolygon.h" #include "qgspolyhedralsurface.h" @@ -59,7 +60,7 @@ std::unique_ptr QgsGeometryFactory::geomFromWkb( QgsConstWk { try { - geom->fromWkb( wkbPtr ); // also updates wkbPtr + geom->fromWkb( wkbPtr ); // also updates wkbPtr } catch ( const QgsWkbException &e ) { @@ -136,6 +137,10 @@ std::unique_ptr QgsGeometryFactory::geomFromWkt( const QStr { geom = std::make_unique< QgsTriangulatedSurface >(); } + else if ( trimmed.startsWith( "NurbsCurve"_L1, Qt::CaseInsensitive ) ) + { + geom = std::make_unique< QgsNurbsCurve >(); + } if ( geom ) { @@ -271,6 +276,8 @@ std::unique_ptr QgsGeometryFactory::geomFromWkbType( Qgis:: return std::make_unique< QgsPolyhedralSurface >(); case Qgis::WkbType::TIN: return std::make_unique< QgsTriangulatedSurface >(); + case Qgis::WkbType::NurbsCurve: + return std::make_unique< QgsNurbsCurve >(); default: return nullptr; } diff --git a/src/core/geometry/qgsgeometryutils.cpp b/src/core/geometry/qgsgeometryutils.cpp index 62b0ba3268c8..dec4cda0db8f 100644 --- a/src/core/geometry/qgsgeometryutils.cpp +++ b/src/core/geometry/qgsgeometryutils.cpp @@ -538,6 +538,62 @@ QgsPoint QgsGeometryUtils::interpolatePointOnArc( const QgsPoint &pt1, const Qgs return QgsPoint( pt1.wkbType(), x, y, z, m ); } +QgsPoint QgsGeometryUtils::interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t ) +{ + const bool hasZ = p0.is3D() && p1.is3D() && p2.is3D() && p3.is3D(); + const bool hasM = p0.isMeasure() && p1.isMeasure() && p2.isMeasure() && p3.isMeasure(); + + double x, y; + double z = std::numeric_limits::quiet_NaN(); + double m = std::numeric_limits::quiet_NaN(); + + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( p0.x(), p0.y(), p0.z(), p0.m(), + p1.x(), p1.y(), p1.z(), p1.m(), + p2.x(), p2.y(), p2.z(), p2.m(), + p3.x(), p3.y(), p3.z(), p3.m(), + t, hasZ, hasM, + x, y, z, m ); + + Qgis::WkbType wkbType = Qgis::WkbType::Point; + if ( hasZ && hasM ) + wkbType = Qgis::WkbType::PointZM; + else if ( hasZ ) + wkbType = Qgis::WkbType::PointZ; + else if ( hasM ) + wkbType = Qgis::WkbType::PointM; + + return QgsPoint( wkbType, x, y, z, m ); +} + +void QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + double p0x, double p0y, double p0z, double p0m, + double p1x, double p1y, double p1z, double p1m, + double p2x, double p2y, double p2z, double p2m, + double p3x, double p3y, double p3z, double p3m, + double t, bool hasZ, bool hasM, + double &outX, double &outY, double &outZ, double &outM ) +{ + // Cubic Bézier formula: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + const double t1 = 1.0 - t; + const double t1_2 = t1 * t1; + const double t1_3 = t1_2 * t1; + const double t_2 = t * t; + const double t_3 = t_2 * t; + + outX = t1_3 * p0x + 3.0 * t1_2 * t * p1x + 3.0 * t1 * t_2 * p2x + t_3 * p3x; + outY = t1_3 * p0y + 3.0 * t1_2 * t * p1y + 3.0 * t1 * t_2 * p2y + t_3 * p3y; + + if ( hasZ ) + { + outZ = t1_3 * p0z + 3.0 * t1_2 * t * p1z + 3.0 * t1 * t_2 * p2z + t_3 * p3z; + } + + if ( hasM ) + { + outM = t1_3 * p0m + 3.0 * t1_2 * t * p1m + 3.0 * t1 * t_2 * p2m + t_3 * p3m; + } +} + bool QgsGeometryUtils::segmentMidPoint( const QgsPoint &p1, const QgsPoint &p2, QgsPoint &result, double radius, const QgsPoint &mousePos ) { const QgsPoint midPoint( ( p1.x() + p2.x() ) / 2.0, ( p1.y() + p2.y() ) / 2.0 ); diff --git a/src/core/geometry/qgsgeometryutils.h b/src/core/geometry/qgsgeometryutils.h index 8afe8f917a62..03885190cd8c 100644 --- a/src/core/geometry/qgsgeometryutils.h +++ b/src/core/geometry/qgsgeometryutils.h @@ -235,6 +235,28 @@ class CORE_EXPORT QgsGeometryUtils */ static QgsPoint interpolatePointOnArc( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3, double distance ) SIP_HOLDGIL; + /** + * Evaluates a point on a cubic Bézier curve defined by four control points. + * + * \param p0 start point (the curve passes through this point) + * \param p1 first control point + * \param p2 second control point + * \param p3 end point (the curve passes through this point) + * \param t parameter value between 0 and 1 + * + * \returns the point on the Bézier curve at parameter \a t + * + * Any Z or M values present in the input points will also be interpolated. + * + * The cubic Bézier formula is: + * \code{.unparsed} + * B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ + * \endcode + * + * \since QGIS 4.0 + */ + static QgsPoint interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t ) SIP_HOLDGIL; + /** * Calculates midpoint on circle passing through \a p1 and \a p2, closest to * the given coordinate \a mousePos. Z dimension is supported and is retrieved from the diff --git a/src/core/geometry/qgsgeometryutils_base.h b/src/core/geometry/qgsgeometryutils_base.h index d89f68c9c889..b1dc8c12e7b4 100644 --- a/src/core/geometry/qgsgeometryutils_base.h +++ b/src/core/geometry/qgsgeometryutils_base.h @@ -95,6 +95,47 @@ class CORE_EXPORT QgsGeometryUtilsBase double *z1 = nullptr, double *z2 = nullptr, double *z = nullptr, double *m1 = nullptr, double *m2 = nullptr, double *m = nullptr ) SIP_SKIP; + /** + * Evaluates a point on a cubic Bézier curve defined by four control points. + * + * This is a high-performance version of the equivalent QgsGeometryUtils method which + * avoids creating a temporary QgsPoint object. + * + * \param p0x start point x + * \param p0y start point y + * \param p0z start point z + * \param p0m start point m + * \param p1x first control point x + * \param p1y first control point y + * \param p1z first control point z + * \param p1m first control point m + * \param p2x second control point x + * \param p2y second control point y + * \param p2z second control point z + * \param p2m second control point m + * \param p3x end point x + * \param p3y end point y + * \param p3z end point z + * \param p3m end point m + * \param t parameter value between 0 and 1 + * \param hasZ whether to calculate a Z value + * \param hasM whether to calculate an M value + * \param outX calculated x-coordinate + * \param outY calculated y-coordinate + * \param outZ calculated z-coordinate + * \param outM calculated m-coordinate + * + * \note Not available in Python bindings + * \since QGIS 4.0 + */ + static void interpolatePointOnCubicBezier( + double p0x, double p0y, double p0z, double p0m, + double p1x, double p1y, double p1z, double p1m, + double p2x, double p2y, double p2z, double p2m, + double p3x, double p3y, double p3z, double p3m, + double t, bool hasZ, bool hasM, + double &outX, double &outY, double &outZ, double &outM ) SIP_SKIP; + /** * Calculates a point a certain \a proportion of the way along the segment from (\a x1, \a y1) to (\a x2, \a y2), * offset from the segment by the specified \a offset amount. diff --git a/src/core/geometry/qgslinestring.cpp b/src/core/geometry/qgslinestring.cpp index dde64e9bfd3a..9da6ee63b636 100644 --- a/src/core/geometry/qgslinestring.cpp +++ b/src/core/geometry/qgslinestring.cpp @@ -180,69 +180,59 @@ QgsLineString::QgsLineString( const QgsLineSegment2D &segment ) mY[1] = segment.endY(); } -static double cubicInterpolate( double a, double b, - double A, double B, double C, double D ) -{ - return A * b * b * b + 3 * B * b * b * a + 3 * C * b * a * a + D * a * a * a; -} - std::unique_ptr< QgsLineString > QgsLineString::fromBezierCurve( const QgsPoint &start, const QgsPoint &controlPoint1, const QgsPoint &controlPoint2, const QgsPoint &end, int segments ) { if ( segments == 0 ) return std::make_unique< QgsLineString >(); - QVector x; - x.resize( segments + 1 ); - QVector y; - y.resize( segments + 1 ); + QVector x( segments + 1 ); + QVector y( segments + 1 ); QVector z; - double *zData = nullptr; - if ( start.is3D() && end.is3D() && controlPoint1.is3D() && controlPoint2.is3D() ) + QVector m; + + const bool hasZ = start.is3D() && controlPoint1.is3D() && controlPoint2.is3D() && end.is3D(); + if ( hasZ ) { z.resize( segments + 1 ); - zData = z.data(); } - QVector m; - double *mData = nullptr; - if ( start.isMeasure() && end.isMeasure() && controlPoint1.isMeasure() && controlPoint2.isMeasure() ) + + const bool hasM = start.isMeasure() && controlPoint1.isMeasure() && controlPoint2.isMeasure() && end.isMeasure(); + if ( hasM ) { m.resize( segments + 1 ); - mData = m.data(); } double *xData = x.data(); double *yData = y.data(); + double *zData = z.data(); // will be nullptr if !hasZ + double *mData = m.data(); // will be nullptr if !hasM + const double step = 1.0 / segments; - double a = 0; - double b = 1.0; - for ( int i = 0; i < segments; i++, a += step, b -= step ) + + for ( int i = 0; i <= segments; ++i ) { - if ( i == 0 ) - { - *xData++ = start.x(); - *yData++ = start.y(); - if ( zData ) - *zData++ = start.z(); - if ( mData ) - *mData++ = start.m(); - } - else - { - *xData++ = cubicInterpolate( a, b, start.x(), controlPoint1.x(), controlPoint2.x(), end.x() ); - *yData++ = cubicInterpolate( a, b, start.y(), controlPoint1.y(), controlPoint2.y(), end.y() ); - if ( zData ) - *zData++ = cubicInterpolate( a, b, start.z(), controlPoint1.z(), controlPoint2.z(), end.z() ); - if ( mData ) - *mData++ = cubicInterpolate( a, b, start.m(), controlPoint1.m(), controlPoint2.m(), end.m() ); - } - } + const double t = i * step; - *xData = end.x(); - *yData = end.y(); - if ( zData ) - *zData = end.z(); - if ( mData ) - *mData = end.m(); + double ix, iy; // interpolated x, y + double iz = std::numeric_limits::quiet_NaN(); + double im = std::numeric_limits::quiet_NaN(); + + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + start.x(), start.y(), start.z(), start.m(), + controlPoint1.x(), controlPoint1.y(), controlPoint1.z(), controlPoint1.m(), + controlPoint2.x(), controlPoint2.y(), controlPoint2.z(), controlPoint2.m(), + end.x(), end.y(), end.z(), end.m(), + t, hasZ, hasM, + ix, iy, iz, im + ); + + *xData++ = ix; + *yData++ = iy; + if ( hasZ ) + *zData++ = iz; + if ( hasM ) + *mData++ = im; + } return std::make_unique< QgsLineString >( x, y, z, m ); } @@ -2358,13 +2348,13 @@ void QgsLineString::sumUpArea3D( double &sum ) const double prevZ = *z++; double normalX = 0.0; - double normalY = 0.0; + double normalY = 0.0; // #spellok - Y component of normal vector double normalZ = 0.0; for ( unsigned int i = 1; i < mX.size(); ++i ) { normalX += prevY * ( *z - prevZ ) - prevZ * ( *y - prevY ); - normalY += prevZ * ( *x - prevX ) - prevX * ( *z - prevZ ); + normalY += prevZ * ( *x - prevX ) - prevX * ( *z - prevZ ); // #spellok normalZ += prevX * ( *y - prevY ) - prevY * ( *x - prevX ); prevX = *x++; @@ -2372,7 +2362,7 @@ void QgsLineString::sumUpArea3D( double &sum ) const prevZ = *z++; } - mSummedUpArea3D = 0.5 * ( normalX * planeNormal.x() + normalY * planeNormal.y() + normalZ * planeNormal.z() ); + mSummedUpArea3D = 0.5 * ( normalX * planeNormal.x() + normalY * planeNormal.y() + normalZ * planeNormal.z() ); // #spellok mHasCachedSummedUpArea3D = true; sum += mSummedUpArea3D; diff --git a/src/core/geometry/qgsnurbscurve.cpp b/src/core/geometry/qgsnurbscurve.cpp new file mode 100644 index 000000000000..60ea266dd4e4 --- /dev/null +++ b/src/core/geometry/qgsnurbscurve.cpp @@ -0,0 +1,1750 @@ +/*************************************************************************** + qgsnurbscurve.cpp + ----------------- + begin : September 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsnurbscurve.h" + +#include +#include +#include +#include +#include +#include + +#include "qgsapplication.h" +#include "qgsbox3d.h" +#include "qgscoordinatetransform.h" +#include "qgsfeedback.h" +#include "qgsgeometrytransformer.h" +#include "qgsgeometryutils.h" +#include "qgsgeometryutils_base.h" +#include "qgslinestring.h" +#include "qgspoint.h" +#include "qgsrectangle.h" +#include "qgswkbptr.h" +#include "qgswkbtypes.h" + +#include + +using namespace nlohmann; + +QgsNurbsCurve::QgsNurbsCurve() +{ + mWkbType = Qgis::WkbType::NurbsCurve; +} + +QgsNurbsCurve::QgsNurbsCurve( const QVector &controlPoints, int degree, const QVector &knots, const QVector &weights ) + : mControlPoints( controlPoints ) + , mKnots( knots ) + , mWeights( weights ) + , mDegree( degree ) +{ + mWkbType = Qgis::WkbType::NurbsCurve; + + // Update WKB type based on coordinate dimensions + if ( !mControlPoints.isEmpty() ) + { + const QgsPoint &firstPoint = mControlPoints.first(); + if ( firstPoint.is3D() ) + mWkbType = QgsWkbTypes::addZ( mWkbType ); + if ( firstPoint.isMeasure() ) + mWkbType = QgsWkbTypes::addM( mWkbType ); + } +} + +QgsNurbsCurve *QgsNurbsCurve::clone() const +{ + return new QgsNurbsCurve( *this ); +} + +bool QgsNurbsCurve::isBezier() const +{ + const int n = mControlPoints.size(); + if ( n < 2 || mDegree < 1 ) + return false; + + if ( mDegree != n - 1 ) + return false; + + if ( !isBSpline() ) + return false; + + if ( mKnots.size() != n + mDegree + 1 ) + return false; + + for ( int i = 0; i <= mDegree; ++i ) + { + if ( !qgsDoubleNear( mKnots[i], 0.0 ) ) + return false; + } + for ( int i = n; i < mKnots.size(); ++i ) + { + if ( !qgsDoubleNear( mKnots[i], 1.0 ) ) + return false; + } + + return true; +} + +bool QgsNurbsCurve::isBSpline() const +{ + for ( const double w : mWeights ) + { + if ( !qgsDoubleNear( w, 1.0 ) ) + return false; + } + return true; +} + +bool QgsNurbsCurve::isRational() const +{ + return !isBSpline(); +} + +bool QgsNurbsCurve::isPolyBezier() const +{ + const int n = mControlPoints.size(); + return mDegree == 3 && n >= 4 && ( n - 1 ) % 3 == 0; +} + +/** + * \brief Find the knot span index for parameter u using binary search. + * + * This is Algorithm A2.1 from "The NURBS Book" (Piegl & Tiller). + * Returns the index i such that knots[i] <= u < knots[i+1]. + * For a NURBS curve with n control points and degree p, valid spans + * are in range [p, n-1]. + * + * \param degree polynomial degree of the NURBS curve + * \param u parameter value to locate + * \param knots knot vector + * \param nPoints number of control points + * \returns knot span index + */ +static int findKnotSpan( const int degree, const double u, const QVector &knots, const int nPoints ) +{ + // Special case: u at or beyond end of parameter range + if ( u >= knots[nPoints] ) + return nPoints - 1; + + // Special case: u at or before start of parameter range + if ( u <= knots[degree] ) + return degree; + + // Binary search for the knot span + int low = degree; + int high = nPoints; + int mid = ( low + high ) / 2; + + while ( u < knots[mid] || u >= knots[mid + 1] ) + { + if ( u < knots[mid] ) + high = mid; + else + low = mid; + mid = ( low + high ) / 2; + } + + return mid; +} + +// Evaluates the NURBS curve at parameter t using De Boor's algorithm. +// Implements Algorithm A4.1 from "The NURBS Book" (Piegl & Tiller). +QgsPoint QgsNurbsCurve::evaluate( double t ) const +{ + const int n = mControlPoints.size(); + if ( n == 0 ) + { + return QgsPoint(); + } + + QString error; + if ( !isValid( error, Qgis::GeometryValidityFlags() ) ) + { + return QgsPoint(); + } + + // Clamp parameter t to valid range [0,1] + if ( t <= 0.0 ) + return mControlPoints.first(); + if ( t >= 1.0 ) + return mControlPoints.last(); + + const bool hasZ = !mControlPoints.isEmpty() && mControlPoints.first().is3D(); + const bool hasM = !mControlPoints.isEmpty() && mControlPoints.first().isMeasure(); + + // Remap parameter from [0,1] to knot vector range [knots[degree], knots[n]] + const double u = mKnots[mDegree] + t * ( mKnots[n] - mKnots[mDegree] ); + + // Find the knot span containing parameter u (Algorithm A2.1) + const int span = findKnotSpan( mDegree, u, mKnots, n ); + + // Temporary arrays for De Boor iteration (degree+1 points) + // Using homogeneous coordinates: (w*x, w*y, w*z, w) for rational curves + // Use std::vector for local temp arrays - no need for Qt's implicit sharing overhead + std::vector tempX( mDegree + 1 ); + std::vector tempY( mDegree + 1 ); + std::vector tempZ( mDegree + 1 ); + std::vector tempM( mDegree + 1 ); + std::vector tempW( mDegree + 1 ); + + // Initialize temp arrays with control points and weights + for ( int j = 0; j <= mDegree; ++j ) + { + const int cpIdx = span - mDegree + j; + const QgsPoint &cp = mControlPoints[cpIdx]; + const double w = ( cpIdx < mWeights.size() ) ? mWeights[cpIdx] : 1.0; + + // Store in homogeneous coordinates (w * P) + tempX[j] = cp.x() * w; + tempY[j] = cp.y() * w; + tempZ[j] = hasZ ? cp.z() * w : 0.0; + tempM[j] = hasM ? cp.m() : 0.0; // M is not weighted + tempW[j] = w; + } + + // De Boor iteration (Algorithm A4.1) in homogeneous space + for ( int k = 1; k <= mDegree; ++k ) + { + for ( int j = mDegree; j >= k; --j ) + { + const int knotIdx = span - mDegree + j; + const double denom = mKnots[knotIdx + mDegree - k + 1] - mKnots[knotIdx]; + + if ( !qgsDoubleNear( denom, 0.0 ) ) + { + const double alpha = ( u - mKnots[knotIdx] ) / denom; + const double oneMinusAlpha = 1.0 - alpha; + + // Linear interpolation in homogeneous space + tempX[j] = oneMinusAlpha * tempX[j - 1] + alpha * tempX[j]; + tempY[j] = oneMinusAlpha * tempY[j - 1] + alpha * tempY[j]; + if ( hasZ ) + tempZ[j] = oneMinusAlpha * tempZ[j - 1] + alpha * tempZ[j]; + if ( hasM ) + tempM[j] = oneMinusAlpha * tempM[j - 1] + alpha * tempM[j]; + + // Interpolate weights + tempW[j] = oneMinusAlpha * tempW[j - 1] + alpha * tempW[j]; + } + } + } + + // Result is in temp[degree], stored in homogeneous coordinates + // Project back to Cartesian by dividing by weight + double x = tempX[mDegree]; + double y = tempY[mDegree]; + double z = tempZ[mDegree]; + double m = tempM[mDegree]; + const double w = tempW[mDegree]; + + if ( !qgsDoubleNear( w, 0.0 ) && !qgsDoubleNear( w, 1.0 ) ) + { + x /= w; + y /= w; + if ( hasZ ) + z /= w; + // M is not divided by weight (it's not in homogeneous space) + } + + // Create point with appropriate dimensionality + if ( hasZ && hasM ) + return QgsPoint( x, y, z, m ); + else if ( hasZ ) + return QgsPoint( x, y, z ); + else if ( hasM ) + return QgsPoint( x, y, std::numeric_limits::quiet_NaN(), m ); + else + return QgsPoint( x, y ); +} + +bool QgsNurbsCurve::isClosed() const +{ + if ( mControlPoints.size() < 2 ) + return false; + + // Check if curve endpoints are the same by evaluating at t=0 and t=1 + const QgsPoint startPt = evaluate( 0.0 ); + const QgsPoint endPt = evaluate( 1.0 ); + + bool closed = qgsDoubleNear( startPt.x(), endPt.x() ) && qgsDoubleNear( startPt.y(), endPt.y() ); + + if ( is3D() && closed ) + closed &= qgsDoubleNear( startPt.z(), endPt.z() ) || ( std::isnan( startPt.z() ) && std::isnan( endPt.z() ) ); + + return closed; +} + +bool QgsNurbsCurve::isClosed2D() const +{ + if ( mControlPoints.size() < 2 ) + return false; + + // Check if curve endpoints are the same in 2D + const QgsPoint startPt = evaluate( 0.0 ); + const QgsPoint endPt = evaluate( 1.0 ); + + return qgsDoubleNear( startPt.x(), endPt.x() ) && qgsDoubleNear( startPt.y(), endPt.y() ); +} + +QgsLineString *QgsNurbsCurve::curveToLine( double tolerance, SegmentationToleranceType toleranceType ) const +{ + Q_UNUSED( toleranceType ); + + // Determine number of segments based on tolerance (angular approximation) + // For NURBS curves, we use uniform parameterization as a first approximation + const int steps = std::max( 2, static_cast( 2 * M_PI / tolerance ) ); + + auto line = new QgsLineString(); + for ( int i = 0; i <= steps; ++i ) + { + const double t = static_cast( i ) / steps; + const QgsPoint pt = evaluate( t ); + line->addVertex( pt ); + } + + return line; +} + +void QgsNurbsCurve::draw( QPainter &p ) const +{ + std::unique_ptr line( curveToLine() ); + if ( line ) + line->draw( p ); +} + +void QgsNurbsCurve::drawAsPolygon( QPainter &p ) const +{ + std::unique_ptr line( curveToLine() ); + if ( line ) + line->drawAsPolygon( p ); +} + +QPolygonF QgsNurbsCurve::asQPolygonF() const +{ + std::unique_ptr line( curveToLine() ); + return line ? line->asQPolygonF() : QPolygonF(); +} + +QgsPoint QgsNurbsCurve::endPoint() const +{ + return mControlPoints.isEmpty() ? QgsPoint() : mControlPoints.last(); +} + +bool QgsNurbsCurve::equals( const QgsCurve &other ) const +{ + if ( geometryType() != other.geometryType() ) + { + return false; + } + + const QgsNurbsCurve *o = qgsgeometry_cast( &other ); + if ( !o ) + return false; + + if ( o->mDegree != mDegree ) + { + return false; + } + + if ( mControlPoints != o->mControlPoints ) + return false; + + if ( mWeights != o->mWeights ) + return false; + + if ( mKnots != o->mKnots ) + return false; + + return true; +} + +int QgsNurbsCurve::indexOf( const QgsPoint &point ) const +{ + for ( int i = 0; i < mControlPoints.size(); ++i ) + { + if ( qgsDoubleNear( mControlPoints[i].distance( point ), 0.0 ) ) + { + return i; + } + } + return -1; +} + +QgsPoint *QgsNurbsCurve::interpolatePoint( double distance ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + { + return nullptr; + } + return line->interpolatePoint( distance ); +} + +int QgsNurbsCurve::numPoints() const +{ + return mControlPoints.size(); +} + +bool QgsNurbsCurve::pointAt( int node, QgsPoint &point, Qgis::VertexType &type ) const +{ + if ( node < 0 || node >= mControlPoints.size() ) + { + return false; + } + point = mControlPoints[node]; + type = Qgis::VertexType::ControlPoint; + return true; +} + +void QgsNurbsCurve::points( QgsPointSequence &pts ) const +{ + pts.reserve( pts.size() + mControlPoints.size() ); + for ( const QgsPoint &p : mControlPoints ) + { + pts.append( p ); + } +} + +QgsCurve *QgsNurbsCurve::reversed() const +{ + auto rev = new QgsNurbsCurve( *this ); + std::reverse( rev->mControlPoints.begin(), rev->mControlPoints.end() ); + std::reverse( rev->mWeights.begin(), rev->mWeights.end() ); + + // Reverse and remap knot vector: new_knot[i] = max_knot + min_knot - old_knot[n-1-i] + if ( !rev->mKnots.isEmpty() ) + { + const double maxKnot = rev->mKnots.last(); + const double minKnot = rev->mKnots.first(); + std::reverse( rev->mKnots.begin(), rev->mKnots.end() ); + for ( double &knot : rev->mKnots ) + { + knot = maxKnot + minKnot - knot; + } + } + + return rev; +} + +void QgsNurbsCurve::scroll( int firstVertexIndex ) +{ + // Scrolling only makes sense for closed curves + if ( !isClosed() || firstVertexIndex <= 0 || firstVertexIndex >= mControlPoints.size() ) + { + return; + } + + // Rotate control points and weights + std::rotate( mControlPoints.begin(), mControlPoints.begin() + firstVertexIndex, mControlPoints.end() ); + std::rotate( mWeights.begin(), mWeights.begin() + firstVertexIndex, mWeights.end() ); + + // Rotate knot vector and adjust values to preserve parameter domain + if ( !mKnots.isEmpty() && firstVertexIndex < mKnots.size() ) + { + const double delta = mKnots[firstVertexIndex] - mKnots[0]; + std::rotate( mKnots.begin(), mKnots.begin() + firstVertexIndex, mKnots.end() ); + // Shift all knot values by -delta to preserve the start parameter + for ( double &knot : mKnots ) + { + knot -= delta; + } + } + + clearCache(); +} + +std::tuple, std::unique_ptr> + QgsNurbsCurve::splitCurveAtVertex( int index ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + { + return std::make_tuple( nullptr, nullptr ); + } + return line->splitCurveAtVertex( index ); +} + +QgsPoint QgsNurbsCurve::startPoint() const +{ + return mControlPoints.isEmpty() ? QgsPoint() : mControlPoints.first(); +} + +void QgsNurbsCurve::sumUpArea( double &sum ) const +{ + // TODO - investigate whether this can be calculated directly + std::unique_ptr line( curveToLine() ); + if ( line ) + line->sumUpArea( sum ); +} + +void QgsNurbsCurve::sumUpArea3D( double &sum ) const +{ + std::unique_ptr line( curveToLine() ); + if ( line ) + line->sumUpArea3D( sum ); +} + +double QgsNurbsCurve::xAt( int index ) const +{ + if ( index < 0 || index >= mControlPoints.size() ) + return 0.0; + return mControlPoints[index].x(); +} + +double QgsNurbsCurve::yAt( int index ) const +{ + if ( index < 0 || index >= mControlPoints.size() ) + return 0.0; + return mControlPoints[index].y(); +} + +double QgsNurbsCurve::zAt( int index ) const +{ + if ( index < 0 || index >= mControlPoints.size() ) + return 0.0; + return mControlPoints[index].is3D() ? mControlPoints[index].z() : std::numeric_limits::quiet_NaN(); +} + +double QgsNurbsCurve::mAt( int index ) const +{ + if ( index < 0 || index >= mControlPoints.size() ) + return 0.0; + return mControlPoints[index].isMeasure() ? mControlPoints[index].m() : std::numeric_limits::quiet_NaN(); +} + +bool QgsNurbsCurve::addZValue( double zValue ) +{ + if ( QgsWkbTypes::hasZ( mWkbType ) ) + return false; + + clearCache(); + mWkbType = QgsWkbTypes::addZ( mWkbType ); + + for ( QgsPoint &p : mControlPoints ) + { + p.addZValue( zValue ); + } + + return true; +} + +bool QgsNurbsCurve::addMValue( double mValue ) +{ + if ( QgsWkbTypes::hasM( mWkbType ) ) + return false; + + clearCache(); + mWkbType = QgsWkbTypes::addM( mWkbType ); + + for ( QgsPoint &p : mControlPoints ) + { + p.addMValue( mValue ); + } + + return true; +} + +bool QgsNurbsCurve::dropZValue() +{ + if ( !is3D() ) + return false; + + for ( QgsPoint &p : mControlPoints ) + { + p.setZ( std::numeric_limits::quiet_NaN() ); + } + + mWkbType = QgsWkbTypes::dropZ( mWkbType ); + clearCache(); + return true; +} + +bool QgsNurbsCurve::dropMValue() +{ + if ( !isMeasure() ) + return false; + + for ( QgsPoint &p : mControlPoints ) + { + p.setM( std::numeric_limits::quiet_NaN() ); + } + + mWkbType = QgsWkbTypes::dropM( mWkbType ); + clearCache(); + return true; +} + +bool QgsNurbsCurve::deleteVertex( QgsVertexId position ) +{ + if ( position.part != 0 || position.ring != 0 ) + { + return false; + } + const int idx = position.vertex; + if ( idx < 0 || idx >= mControlPoints.size() ) + { + return false; + } + mControlPoints.remove( idx ); + if ( idx < mWeights.size() ) + { + mWeights.remove( idx ); + } + + generateUniformKnots(); + + clearCache(); + return true; +} + +void QgsNurbsCurve::filterVertices( const std::function &filter ) +{ + QVector newPts; + QVector newWeights; + for ( int i = 0; i < mControlPoints.size(); ++i ) + { + if ( filter( mControlPoints[i] ) ) + { + newPts.append( mControlPoints[i] ); + if ( i < mWeights.size() ) + newWeights.append( mWeights[i] ); + } + } + mControlPoints = newPts; + mWeights = newWeights; + + generateUniformKnots(); + + clearCache(); +} + +void QgsNurbsCurve::generateUniformKnots() +{ + const int n = mControlPoints.size(); + const int knotsSize = n + mDegree + 1; + mKnots.clear(); + mKnots.reserve( knotsSize ); + for ( int i = 0; i < knotsSize; ++i ) + { + if ( i <= mDegree ) + mKnots.append( 0.0 ); + else if ( i >= n ) + mKnots.append( 1.0 ); + else + mKnots.append( static_cast( i - mDegree ) / ( n - mDegree ) ); + } +} + +bool QgsNurbsCurve::fromWkb( QgsConstWkbPtr &wkb ) +{ + clear(); + + if ( !wkb ) + return false; + + // Store header endianness + const unsigned char headerEndianness = *static_cast( wkb ); + + Qgis::WkbType type = wkb.readHeader(); + if ( QgsWkbTypes::flatType( type ) != Qgis::WkbType::NurbsCurve ) + return false; + + mWkbType = type; + const bool is3D = QgsWkbTypes::hasZ( type ); + const bool isMeasure = QgsWkbTypes::hasM( type ); + + // Read degree (4 bytes uint32) + quint32 degree; + wkb >> degree; + + // Validate degree before casting to int + if ( degree < 1 || degree > static_cast( std::numeric_limits::max() ) ) + return false; + + mDegree = static_cast( degree ); + + // Read number of control points (4 bytes uint32) + quint32 numControlPoints; + wkb >> numControlPoints; + + // Sanity check: numControlPoints should be reasonable given the WKB blob size + // Each control point needs at least: + // - 1 byte (endianness) + // - 16 bytes (x,y) + // - 8 bytes (z) if 3D + // - 8 bytes (m) if measure + // - 1 byte (weight flag) + // Minimum: 18 bytes (2D) to 34 bytes (ZM) + const int minBytesPerPoint = 18 + ( is3D ? 8 : 0 ) + ( isMeasure ? 8 : 0 ); + if ( numControlPoints > static_cast( wkb.remaining() / minBytesPerPoint + 1 ) ) + return false; + + mControlPoints.clear(); + mWeights.clear(); + mControlPoints.reserve( numControlPoints ); + mWeights.reserve( numControlPoints ); + + // Read control points + for ( quint32 i = 0; i < numControlPoints; ++i ) + { + // Read byte order for this point (1 byte) + char pointEndianness; + wkb >> pointEndianness; + + // Validate endianness: must be 0 (big-endian) or 1 (little-endian) + // and must match the WKB header endianness + if ( static_cast( pointEndianness ) != headerEndianness ) + return false; + + // Read coordinates + double x, y, z = 0.0, m = 0.0; + wkb >> x >> y; + + if ( is3D ) + wkb >> z; + if ( isMeasure ) + wkb >> m; + + // Read weight flag (1 byte) + char weightFlag; + wkb >> weightFlag; + + double weight = 1.0; + if ( weightFlag == 1 ) + { + // Read custom weight (8 bytes double) + wkb >> weight; + } + + // Create point with appropriate dimensionality + QgsPoint point; + if ( is3D && isMeasure ) + point = QgsPoint( x, y, z, m ); + else if ( is3D ) + point = QgsPoint( x, y, z ); + else if ( isMeasure ) + point = QgsPoint( x, y, std::numeric_limits::quiet_NaN(), m ); + else + point = QgsPoint( x, y ); + + mControlPoints.append( point ); + mWeights.append( weight ); + } + + // Read number of knots (4 bytes uint32) + quint32 numKnots; + wkb >> numKnots; + + // Sanity check: numKnots should be numControlPoints + degree + 1 + const quint32 expectedKnots = numControlPoints + degree + 1; + if ( numKnots != expectedKnots ) + return false; + + // Sanity check: remaining WKB should have enough bytes for knots + if ( numKnots * sizeof( double ) > static_cast( wkb.remaining() ) ) + return false; + + mKnots.clear(); + mKnots.reserve( numKnots ); + + // Read knot values (8 bytes double each) + for ( quint32 i = 0; i < numKnots; ++i ) + { + double knot; + wkb >> knot; + mKnots.append( knot ); + } + + return true; +} + +bool QgsNurbsCurve::fromWkt( const QString &wkt ) +{ + clear(); + + const QString geomTypeStr = wkt.split( '(' )[0].trimmed().toUpper(); + + if ( !geomTypeStr.startsWith( "NURBSCURVE"_L1 ) ) + { + return false; + } + + // Determine dimensionality from the geometry type string + // Handle both "NURBSCURVEZM" and "NURBSCURVE ZM" formats + if ( geomTypeStr.contains( "ZM"_L1 ) ) + mWkbType = Qgis::WkbType::NurbsCurveZM; + else if ( geomTypeStr.endsWith( 'Z' ) || geomTypeStr.endsWith( " Z"_L1 ) ) + mWkbType = Qgis::WkbType::NurbsCurveZ; + else if ( geomTypeStr.endsWith( 'M' ) || geomTypeStr.endsWith( " M"_L1 ) ) + mWkbType = Qgis::WkbType::NurbsCurveM; + else + mWkbType = Qgis::WkbType::NurbsCurve; + + QPair parts = QgsGeometryUtils::wktReadBlock( wkt ); + + if ( parts.second.compare( "EMPTY"_L1, Qt::CaseInsensitive ) == 0 || parts.second.isEmpty() ) + return true; + + // Split the content by commas at parentheses level 0 + QStringList blocks = QgsGeometryUtils::wktGetChildBlocks( parts.second, QString() ); + + if ( blocks.isEmpty() ) + return false; + + // First block should be the degree + bool ok = true; + int degree = blocks[0].trimmed().toInt( &ok ); + if ( !ok || degree < 1 ) + return false; + + if ( blocks.size() < 2 ) + return false; + + // Second block should be the control points + QString pointsStr = blocks[1].trimmed(); + + // Validate control points block starts with '(' and ends with ')' + if ( !pointsStr.startsWith( '('_L1 ) || !pointsStr.endsWith( ')'_L1 ) ) + return false; + + pointsStr = pointsStr.mid( 1, pointsStr.length() - 2 ).trimmed(); + + // Parse control points + QStringList pointsCoords = pointsStr.split( ',', Qt::SkipEmptyParts ); + QVector controlPoints; + + const thread_local QRegularExpression rx( u"\\s+"_s ); + + for ( const QString &pointStr : pointsCoords ) + { + QStringList coords = pointStr.trimmed().split( rx, Qt::SkipEmptyParts ); + + if ( coords.size() < 2 ) + return false; + + QgsPoint point; + bool ok = true; + + double x = coords[0].toDouble( &ok ); + if ( !ok ) + return false; + + double y = coords[1].toDouble( &ok ); + if ( !ok ) + return false; + + // Handle different coordinate patterns based on declared geometry type + if ( coords.size() >= 3 ) + { + if ( isMeasure() && !is3D() && coords.size() == 3 ) + { + // NURBSCURVE M pattern: (x y m) - third coordinate is M, not Z + double m = coords[2].toDouble( &ok ); + if ( !ok ) + return false; + point = QgsPoint( x, y, std::numeric_limits::quiet_NaN(), m ); + } + else if ( is3D() && !isMeasure() && coords.size() >= 3 ) + { + // NURBSCURVE Z pattern: (x y z) + double z = coords[2].toDouble( &ok ); + if ( !ok ) + return false; + point = QgsPoint( x, y, z ); + } + else if ( is3D() && isMeasure() && coords.size() >= 4 ) + { + // NURBSCURVE ZM pattern: (x y z m) + double z = coords[2].toDouble( &ok ); + if ( !ok ) + return false; + double m = coords[3].toDouble( &ok ); + if ( !ok ) + return false; + point = QgsPoint( x, y, z, m ); + } + else if ( isMeasure() && coords.size() >= 4 ) + { + // NURBSCURVE M pattern with 4 coords: (x y z m) - upgrade to ZM + double z = coords[2].toDouble( &ok ); + if ( !ok ) + return false; + double m = coords[3].toDouble( &ok ); + if ( !ok ) + return false; + point = QgsPoint( x, y, z, m ); + if ( !is3D() ) + mWkbType = QgsWkbTypes::addZ( mWkbType ); + } + else if ( !is3D() && !isMeasure() && coords.size() == 3 ) + { + // No explicit dimension - auto-upgrade to 3D: (x y z) + double z = coords[2].toDouble( &ok ); + if ( !ok ) + return false; + point = QgsPoint( x, y, z ); + mWkbType = QgsWkbTypes::addZ( mWkbType ); + } + else if ( !is3D() && !isMeasure() && coords.size() >= 4 ) + { + // No explicit dimension - auto-upgrade to ZM: (x y z m) + double z = coords[2].toDouble( &ok ); + if ( !ok ) + return false; + double m = coords[3].toDouble( &ok ); + if ( !ok ) + return false; + point = QgsPoint( x, y, z, m ); + mWkbType = QgsWkbTypes::addZ( mWkbType ); + mWkbType = QgsWkbTypes::addM( mWkbType ); + } + else + { + // Only 2 coordinates but declared type may require Z or M + if ( is3D() && isMeasure() ) + point = QgsPoint( Qgis::WkbType::PointZM, x, y, 0.0, 0.0 ); + else if ( is3D() ) + point = QgsPoint( Qgis::WkbType::PointZ, x, y, 0.0 ); + else if ( isMeasure() ) + point = QgsPoint( Qgis::WkbType::PointM, x, y, std::numeric_limits::quiet_NaN(), 0.0 ); + else + point = QgsPoint( x, y ); + } + } + else + { + // Only 2 coordinates - create point matching declared type + if ( is3D() && isMeasure() ) + point = QgsPoint( Qgis::WkbType::PointZM, x, y, 0.0, 0.0 ); + else if ( is3D() ) + point = QgsPoint( Qgis::WkbType::PointZ, x, y, 0.0 ); + else if ( isMeasure() ) + point = QgsPoint( Qgis::WkbType::PointM, x, y, std::numeric_limits::quiet_NaN(), 0.0 ); + else + point = QgsPoint( x, y ); + } + + controlPoints.append( point ); + } + + mControlPoints = controlPoints; + + // Initialize weights to 1.0 (non-rational by default) + mWeights.clear(); + for ( int i = 0; i < controlPoints.size(); ++i ) + { + mWeights.append( 1.0 ); + } + + // Parse additional parameters (degree already parsed at the beginning) + bool hasWeights = false; + bool hasKnots = false; + + // Process remaining blocks (starting from index 2 since 0=degree, 1=control points) + for ( int i = 2; i < blocks.size(); ++i ) + { + QString block = blocks[i].trimmed(); + + if ( block.startsWith( '('_L1 ) ) + { + // Validate block ends with ')' + if ( !block.endsWith( ')'_L1 ) ) + return false; + + // This could be weights or knots vector + block = block.mid( 1, block.length() - 2 ).trimmed(); + QStringList values = block.split( ',', Qt::SkipEmptyParts ); + + QVector parsedValues; + for ( const QString &valueStr : values ) + { + bool ok = true; + double value = valueStr.trimmed().toDouble( &ok ); + if ( !ok ) + return false; + parsedValues.append( value ); + } + + if ( !hasWeights && parsedValues.size() == controlPoints.size() ) + { + // This is the weights vector + mWeights = parsedValues; + hasWeights = true; + } + else if ( !hasKnots ) + { + // This is the knots vector + mKnots = parsedValues; + hasKnots = true; + } + } + else + { + // Invalid block - doesn't start with '(' + return false; + } + } + + mDegree = degree; + + // Validate: need at least (degree + 1) control points + if ( controlPoints.size() <= degree ) + return false; + + // If no knots were provided, create default knots (open uniform) + if ( !hasKnots ) + { + generateUniformKnots(); + } + + return true; +} + +bool QgsNurbsCurve::fuzzyEqual( const QgsAbstractGeometry &other, double epsilon ) const +{ + const QgsNurbsCurve *o = qgsgeometry_cast( &other ); + if ( !o ) + return false; + + if ( mDegree != o->mDegree || mControlPoints.size() != o->mControlPoints.size() || mWeights.size() != o->mWeights.size() || mKnots.size() != o->mKnots.size() ) + { + return false; + } + + for ( int i = 0; i < mControlPoints.size(); ++i ) + { + if ( mControlPoints[i].distance( o->mControlPoints[i] ) >= epsilon ) + return false; + } + + for ( int i = 0; i < mWeights.size(); ++i ) + { + if ( std::fabs( mWeights[i] - o->mWeights[i] ) > epsilon ) + return false; + } + + for ( int i = 0; i < mKnots.size(); ++i ) + { + if ( std::fabs( mKnots[i] - o->mKnots[i] ) > epsilon ) + return false; + } + + return true; +} + +bool QgsNurbsCurve::fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon ) const +{ + return fuzzyEqual( other, epsilon ); +} + +QString QgsNurbsCurve::geometryType() const +{ + return u"NurbsCurve"_s; +} + +bool QgsNurbsCurve::hasCurvedSegments() const +{ + return true; +} + +int QgsNurbsCurve::partCount() const +{ + return 1; +} + +QgsCurve *QgsNurbsCurve::toCurveType() const +{ + return new QgsNurbsCurve( *this ); +} + +QgsPoint QgsNurbsCurve::vertexAt( QgsVertexId id ) const +{ + if ( id.part != 0 || id.ring != 0 ) + { + return QgsPoint(); + } + const int idx = id.vertex; + if ( idx < 0 || idx >= mControlPoints.size() ) + { + return QgsPoint(); + } + return mControlPoints[idx]; +} + +int QgsNurbsCurve::vertexCount( int part, int ring ) const +{ + return ( part == 0 && ring == 0 ) ? mControlPoints.size() : 0; +} + +int QgsNurbsCurve::vertexNumberFromVertexId( QgsVertexId id ) const +{ + if ( id.part == 0 && id.ring == 0 ) + { + return id.vertex; + } + return -1; +} + +bool QgsNurbsCurve::isValid( QString &error, Qgis::GeometryValidityFlags flags ) const +{ + Q_UNUSED( flags ); + + // Use cached validity if available + if ( mValidityComputed ) + { + if ( !mIsValid ) + error = u"NURBS curve is invalid"_s; + return mIsValid; + } + + mValidityComputed = true; + mIsValid = false; + + if ( mDegree < 1 ) + { + error = u"Degree must be >= 1"_s; + return false; + } + + const int n = mControlPoints.size(); + if ( n < mDegree + 1 ) + { + error = u"Not enough control points for degree"_s; + return false; + } + + if ( mKnots.size() != n + mDegree + 1 ) + { + error = u"Knot vector size is incorrect"_s; + return false; + } + + if ( mWeights.size() != n ) + { + error = u"Weights vector size mismatch"_s; + return false; + } + + // Check that knots are non-decreasing + for ( int i = 1; i < mKnots.size(); ++i ) + { + if ( mKnots[i] < mKnots[i - 1] ) + { + error = u"Knot vector values must be non-decreasing"_s; + return false; + } + } + + mIsValid = true; + return true; +} + +void QgsNurbsCurve::addToPainterPath( QPainterPath &path ) const +{ + std::unique_ptr line( curveToLine() ); + if ( line ) + line->addToPainterPath( path ); +} + +QgsCurve *QgsNurbsCurve::curveSubstring( double startDistance, double endDistance ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + return nullptr; + return line->curveSubstring( startDistance, endDistance ); +} + +double QgsNurbsCurve::length() const +{ + std::unique_ptr line( curveToLine() ); + return line ? line->length() : 0.0; +} + +double QgsNurbsCurve::segmentLength( QgsVertexId startVertex ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + return 0.0; + return line->segmentLength( startVertex ); +} + +double QgsNurbsCurve::distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + return -1.0; + return line->distanceBetweenVertices( fromVertex, toVertex ); +} + +QgsAbstractGeometry *QgsNurbsCurve::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const +{ + auto result = new QgsNurbsCurve( *this ); + for ( QgsPoint &pt : result->mControlPoints ) + { + if ( hSpacing > 0 ) + pt.setX( std::round( pt.x() / hSpacing ) * hSpacing ); + if ( vSpacing > 0 ) + pt.setY( std::round( pt.y() / vSpacing ) * vSpacing ); + if ( pt.is3D() && dSpacing > 0 ) + pt.setZ( std::round( pt.z() / dSpacing ) * dSpacing ); + if ( pt.isMeasure() && mSpacing > 0 ) + pt.setM( std::round( pt.m() / mSpacing ) * mSpacing ); + } + + if ( removeRedundantPoints ) + result->removeDuplicateNodes(); + + return result; +} + +QgsAbstractGeometry *QgsNurbsCurve::simplifyByDistance( double tolerance ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + return new QgsNurbsCurve( *this ); + return line->simplifyByDistance( tolerance ); +} + +bool QgsNurbsCurve::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + if ( mControlPoints.size() < 2 ) + return false; + + QVector newPoints; + QVector newWeights; + + newPoints.reserve( mControlPoints.size() ); + newWeights.reserve( mWeights.size() ); + + newPoints.append( mControlPoints.first() ); + if ( !mWeights.isEmpty() ) + newWeights.append( mWeights.first() ); + + for ( int i = 1; i < mControlPoints.size(); ++i ) + { + const double dist = ( useZValues && mControlPoints[i].is3D() && mControlPoints[i - 1].is3D() ) + ? mControlPoints[i].distance3D( mControlPoints[i - 1] ) + : mControlPoints[i].distance( mControlPoints[i - 1] ); + + if ( dist >= epsilon ) + { + newPoints.append( mControlPoints[i] ); + if ( i < mWeights.size() ) + newWeights.append( mWeights[i] ); + } + } + + const bool changed = ( newPoints.size() != mControlPoints.size() ); + if ( !changed ) + return false; + + mControlPoints = newPoints; + mWeights = newWeights; + + // Regenerate uniform knot vector for the new number of control points + generateUniformKnots(); + + clearCache(); + return true; +} + +double QgsNurbsCurve::vertexAngle( QgsVertexId vertex ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + return 0.0; + return line->vertexAngle( vertex ); +} + +void QgsNurbsCurve::swapXy() +{ + for ( QgsPoint &pt : mControlPoints ) + { + const double x = pt.x(); + pt.setX( pt.y() ); + pt.setY( x ); + } + clearCache(); +} + +bool QgsNurbsCurve::transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback ) +{ + Q_UNUSED( feedback ); + if ( !transformer ) + return false; + + for ( QgsPoint &pt : mControlPoints ) + { + double x = pt.x(), y = pt.y(), z = pt.z(), m = pt.m(); + if ( !transformer->transformPoint( x, y, z, m ) ) + return false; + pt.setX( x ); + pt.setY( y ); + pt.setZ( z ); + pt.setM( m ); + } + + clearCache(); + return true; +} + +QgsAbstractGeometry *QgsNurbsCurve::createEmptyWithSameType() const +{ + return new QgsNurbsCurve(); +} + +double QgsNurbsCurve::closestSegment( const QgsPoint &pt, QgsPoint &segmentPt, QgsVertexId &vertexAfter, int *leftOf, double epsilon ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + { + segmentPt = QgsPoint(); + vertexAfter = QgsVertexId(); + if ( leftOf ) + *leftOf = 0; + return -1; + } + return line->closestSegment( pt, segmentPt, vertexAfter, leftOf, epsilon ); +} + +void QgsNurbsCurve::transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d, bool transformZ ) +{ + for ( QgsPoint &pt : mControlPoints ) + { + double x = pt.x(); + double y = pt.y(); + double z = transformZ && pt.is3D() ? pt.z() : std::numeric_limits::quiet_NaN(); + ct.transformInPlace( x, y, z, d ); + pt.setX( x ); + pt.setY( y ); + if ( transformZ && pt.is3D() ) + pt.setZ( z ); + } + clearCache(); +} + +void QgsNurbsCurve::transform( const QTransform &t, double zTranslate, double zScale, double mTranslate, double mScale ) +{ + for ( QgsPoint &pt : mControlPoints ) + { + const QPointF p = t.map( QPointF( pt.x(), pt.y() ) ); + pt.setX( p.x() ); + pt.setY( p.y() ); + + if ( pt.is3D() ) + pt.setZ( pt.z() * zScale + zTranslate ); + if ( pt.isMeasure() ) + pt.setM( pt.m() * mScale + mTranslate ); + } + clearCache(); +} + +QgsRectangle QgsNurbsCurve::boundingBox() const +{ + return boundingBox3D().toRectangle(); +} + +QgsBox3D QgsNurbsCurve::boundingBox3D() const +{ + if ( mBoundingBox.isNull() ) + { + mBoundingBox = calculateBoundingBox3D(); + } + return mBoundingBox; +} + +QgsBox3D QgsNurbsCurve::calculateBoundingBox3D() const +{ + if ( mControlPoints.isEmpty() ) + return QgsBox3D(); + + // The bounding box must include all control points, not just points on the curve. + // This is important for Snapping to control points (they can lie outside the curve itself) + QgsBox3D bbox; + for ( const QgsPoint &pt : mControlPoints ) + { + bbox.combineWith( pt.x(), pt.y(), pt.is3D() ? pt.z() : std::numeric_limits::quiet_NaN() ); + } + + // Also include points on the curve to ensure the bbox is complete + std::unique_ptr line( curveToLine() ); + if ( line ) + { + bbox.combineWith( line->boundingBox3D() ); + } + + return bbox; +} + +void QgsNurbsCurve::clearCache() const +{ + QgsCurve::clearCache(); + mValidityComputed = false; + mIsValid = false; +} + +bool QgsNurbsCurve::moveVertex( QgsVertexId position, const QgsPoint &newPos ) +{ + if ( position.part != 0 || position.ring != 0 ) + return false; + + const int idx = position.vertex; + if ( idx < 0 || idx >= mControlPoints.size() ) + return false; + + mControlPoints[idx] = newPos; + clearCache(); + return true; +} + +bool QgsNurbsCurve::insertVertex( QgsVertexId position, const QgsPoint &vertex ) +{ + if ( position.part != 0 || position.ring != 0 ) + return false; + + const int idx = position.vertex; + if ( idx < 0 || idx > mControlPoints.size() ) + return false; + + mControlPoints.insert( idx, vertex ); + if ( idx <= mWeights.size() ) + mWeights.insert( idx, 1.0 ); + + generateUniformKnots(); + + clearCache(); + return true; +} + +int QgsNurbsCurve::wkbSize( QgsAbstractGeometry::WkbFlags flags ) const +{ + Q_UNUSED( flags ); + + const bool is3D = QgsWkbTypes::hasZ( mWkbType ); + const bool isMeasure = QgsWkbTypes::hasM( mWkbType ); + const int coordinateDimension = 2 + ( is3D ? 1 : 0 ) + ( isMeasure ? 1 : 0 ); + + int size = 0; + + // WKB header (endianness + type) + size += 1 + 4; + + // Degree (4 bytes) + size += 4; + + // Number of control points (4 bytes) + size += 4; + + // Control points data + for ( int i = 0; i < mControlPoints.size(); ++i ) + { + // Point byte order (1 byte) + size += 1; + + // Coordinates (8 bytes per coordinate) + size += coordinateDimension * 8; + + // Weight flag (1 byte) + size += 1; + + // Weight value if not default (8 bytes) + if ( i < mWeights.size() && std::fabs( mWeights[i] - 1.0 ) > 1e-10 ) + size += 8; + } + + // Number of knots (4 bytes) + size += 4; + + // Knot values (8 bytes each) + size += mKnots.size() * 8; + + return size; +} + +QByteArray QgsNurbsCurve::asWkb( QgsAbstractGeometry::WkbFlags flags ) const +{ + QByteArray wkbArray; + wkbArray.resize( QgsNurbsCurve::wkbSize( flags ) ); + QgsWkbPtr wkbPtr( wkbArray ); + + // Write WKB header + wkbPtr << static_cast( QgsApplication::endian() ); + wkbPtr << static_cast( mWkbType ); + + // Write degree (4 bytes uint32) + wkbPtr << static_cast( mDegree ); + + // Write number of control points (4 bytes uint32) + wkbPtr << static_cast( mControlPoints.size() ); + + const bool is3D = QgsWkbTypes::hasZ( mWkbType ); + const bool isMeasure = QgsWkbTypes::hasM( mWkbType ); + + // Write control points + for ( int i = 0; i < mControlPoints.size(); ++i ) + { + const QgsPoint &point = mControlPoints[i]; + + // Write byte order for this point (1 byte) - use same as global + wkbPtr << static_cast( QgsApplication::endian() ); + + // Write coordinates + wkbPtr << point.x() << point.y(); + + if ( is3D ) + wkbPtr << point.z(); + if ( isMeasure ) + wkbPtr << point.m(); + + // Write weight flag and weight + const double weight = ( i < mWeights.size() ) ? mWeights[i] : 1.0; + const bool hasCustomWeight = std::fabs( weight - 1.0 ) > 1e-10; + + wkbPtr << static_cast( hasCustomWeight ? 1 : 0 ); + + if ( hasCustomWeight ) + { + wkbPtr << weight; + } + } + + // Write number of knots (4 bytes uint32) + wkbPtr << static_cast( mKnots.size() ); + + // Write knot values (8 bytes double each) + for ( const double knot : mKnots ) + { + wkbPtr << knot; + } + + return wkbArray; +} + +QString QgsNurbsCurve::asWkt( int precision ) const +{ + QString wkt = wktTypeStr(); + + if ( isEmpty() ) + { + wkt += " EMPTY"_L1; + } + else + { + wkt += " ("_L1; + + // Add degree first + wkt += QString::number( mDegree ); + + // Add control points + wkt += ", ("_L1; + for ( int i = 0; i < mControlPoints.size(); ++i ) + { + if ( i > 0 ) + wkt += ", "_L1; + + const QgsPoint &pt = mControlPoints[i]; + wkt += qgsDoubleToString( pt.x(), precision ) + ' ' + qgsDoubleToString( pt.y(), precision ); + + if ( pt.is3D() ) + wkt += ' ' + qgsDoubleToString( pt.z(), precision ); + + if ( pt.isMeasure() ) + wkt += ' ' + qgsDoubleToString( pt.m(), precision ); + } + wkt += ')'; + + // Always add weights if they exist to ensure round-trip consistency + if ( !mWeights.isEmpty() ) + { + wkt += ", ("_L1; + for ( int i = 0; i < mWeights.size(); ++i ) + { + if ( i > 0 ) + wkt += ", "_L1; + wkt += qgsDoubleToString( mWeights[i], precision ); + } + wkt += ')'; + } + + // Always add knots if they exist to ensure round-trip consistency + if ( !mKnots.isEmpty() ) + { + wkt += ", ("_L1; + for ( int i = 0; i < mKnots.size(); ++i ) + { + if ( i > 0 ) + wkt += ", "_L1; + wkt += qgsDoubleToString( mKnots[i], precision ); + } + wkt += ')'; + } + + wkt += ')'; + } + + return wkt; +} + +QDomElement QgsNurbsCurve::asGml2( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const +{ + // GML2 does not support NURBS curves, convert to LineString + // TODO: GML3 has BSpline support, but it's not clear how it's handled elsewhere in QGIS + std::unique_ptr line( curveToLine() ); + if ( !line ) + return QDomElement(); + return line->asGml2( doc, precision, ns, axisOrder ); +} + +QDomElement QgsNurbsCurve::asGml3( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const +{ + // TODO: GML3 has native BSpline support (gml:BSpline), but it's not clear how it's handled elsewhere in QGIS + // For now, convert to LineString for compatibility + std::unique_ptr line( curveToLine() ); + if ( !line ) + return QDomElement(); + return line->asGml3( doc, precision, ns, axisOrder ); +} + +json QgsNurbsCurve::asJsonObject( int precision ) const +{ + std::unique_ptr line( curveToLine() ); + if ( !line ) + return json::object(); + return line->asJsonObject( precision ); +} + +QString QgsNurbsCurve::asKml( int precision ) const +{ + // KML does not support NURBS curves, convert to LineString + std::unique_ptr line( curveToLine() ); + if ( !line ) + return QString(); + return line->asKml( precision ); +} + +int QgsNurbsCurve::dimension() const +{ + return 1; +} + +bool QgsNurbsCurve::isEmpty() const +{ + return mControlPoints.isEmpty(); +} + +void QgsNurbsCurve::clear() +{ + mControlPoints.clear(); + mKnots.clear(); + mWeights.clear(); + mDegree = 0; + clearCache(); +} + +bool QgsNurbsCurve::boundingBoxIntersects( const QgsRectangle &rectangle ) const +{ + return boundingBox().intersects( rectangle ); +} + +bool QgsNurbsCurve::boundingBoxIntersects( const QgsBox3D &box3d ) const +{ + return boundingBox3D().intersects( box3d ); +} + +QgsPoint QgsNurbsCurve::centroid() const +{ + std::unique_ptr line( curveToLine() ); + return line ? line->centroid() : QgsPoint(); +} + +int QgsNurbsCurve::compareToSameClass( const QgsAbstractGeometry *other ) const +{ + const QgsNurbsCurve *otherCurve = qgsgeometry_cast( other ); + if ( !otherCurve ) + return -1; + + if ( mDegree < otherCurve->mDegree ) + return -1; + else if ( mDegree > otherCurve->mDegree ) + return 1; + + const int nThis = mControlPoints.size(); + const int nOther = otherCurve->mControlPoints.size(); + + if ( nThis < nOther ) + return -1; + else if ( nThis > nOther ) + return 1; + + for ( int i = 0; i < nThis; ++i ) + { + if ( mControlPoints[i].x() < otherCurve->mControlPoints[i].x() ) + return -1; + if ( mControlPoints[i].x() > otherCurve->mControlPoints[i].x() ) + return 1; + if ( mControlPoints[i].y() < otherCurve->mControlPoints[i].y() ) + return -1; + if ( mControlPoints[i].y() > otherCurve->mControlPoints[i].y() ) + return 1; + } + + if ( mWeights.size() < otherCurve->mWeights.size() ) + return -1; + else if ( mWeights.size() > otherCurve->mWeights.size() ) + return 1; + + for ( int i = 0; i < mWeights.size(); ++i ) + { + if ( mWeights[i] < otherCurve->mWeights[i] ) + return -1; + else if ( mWeights[i] > otherCurve->mWeights[i] ) + return 1; + } + + if ( mKnots.size() < otherCurve->mKnots.size() ) + return -1; + else if ( mKnots.size() > otherCurve->mKnots.size() ) + return 1; + + for ( int i = 0; i < mKnots.size(); ++i ) + { + if ( mKnots[i] < otherCurve->mKnots[i] ) + return -1; + else if ( mKnots[i] > otherCurve->mKnots[i] ) + return 1; + } + + return 0; +} + +double QgsNurbsCurve::weight( int index ) const +{ + if ( index < 0 || index >= mWeights.size() ) + return 1.0; + return mWeights[index]; +} + +bool QgsNurbsCurve::setWeight( int index, double weight ) +{ + if ( index < 0 || index >= mWeights.size() ) + return false; + if ( weight <= 0.0 ) + return false; + mWeights[index] = weight; + clearCache(); + return true; +} diff --git a/src/core/geometry/qgsnurbscurve.h b/src/core/geometry/qgsnurbscurve.h new file mode 100644 index 000000000000..07778a83d390 --- /dev/null +++ b/src/core/geometry/qgsnurbscurve.h @@ -0,0 +1,371 @@ +/*************************************************************************** + qgsnurbscurve.h + ----------------- + begin : September 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSNURBSCURVE_H +#define QGSNURBSCURVE_H + +#include +#include + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgscurve.h" + +#include + +/** + * \ingroup core + * \class QgsNurbsCurve + * \brief Represents a NURBS (Non-Uniform Rational B-Spline) curve geometry in 2D/3D. + * + * NURBS curves are a mathematical model commonly used in computer graphics + * for representing curves. They are parametric curves defined by control points, + * weights, knot vectors, and a degree. + * + * \since QGIS 4.0 + */ +class CORE_EXPORT QgsNurbsCurve : public QgsCurve +{ + public: + /** + * Constructor for an empty NURBS curve geometry. + */ + QgsNurbsCurve(); + + /** + * Constructs a NURBS curve from control points, degree, knot vector and weights. + * \param controlPoints control points defining the curve. The number of control points must be strictly greater than \a degree + * \param degree degree of the NURBS curve (must be >= 1, typically 1-3) + * \param knots knot vector (must have size = control points count + degree + 1, values must be non-decreasing) + * \param weights weight vector for rational curves (same size as control points) + */ + QgsNurbsCurve( const QVector &controlPoints, int degree, const QVector &knots, const QVector &weights ); + + QgsNurbsCurve *clone() const override SIP_FACTORY; + +#ifndef SIP_RUN + + /** + * Evaluates the NURBS curve at parameter t ∈ [0,1]. + * Uses the Cox-de Boor algorithm for B-spline basis function evaluation. + * \param t parameter value between 0 and 1 + * \returns point on the curve at parameter t + */ + [[nodiscard]] QgsPoint evaluate( double t ) const; +#else + + /** + * Evaluates the NURBS curve at parameter t ∈ [0,1]. + * Uses the Cox-de Boor algorithm for B-spline basis function evaluation. + * \param t parameter value between 0 and 1 + * \returns point on the curve at parameter t + * \throws ValueError if t is not in range [0, 1] + */ + SIP_PYOBJECT evaluate( double t ) const SIP_TYPEHINT( QgsPoint ); + % MethodCode + if ( a0 < 0.0 || a0 > 1.0 ) + { + PyErr_SetString( PyExc_ValueError, "Parameter t must be in range [0, 1]" ); + sipIsErr = 1; + } + else + { + sipRes = sipConvertFromType( new QgsPoint( sipCpp->evaluate( a0 ) ), sipType_QgsPoint, Py_None ); + } + % End +#endif + + /** + * Returns TRUE if this curve represents a Bézier curve. + * A Bézier curve is a special case of NURBS with uniform weights and specific knot vector. + */ + bool isBezier() const SIP_HOLDGIL; + + /** + * Returns TRUE if this curve represents a B-spline (non-rational NURBS). + */ + bool isBSpline() const SIP_HOLDGIL; + + /** + * Returns TRUE if this curve is rational (has non-uniform weights). + */ + bool isRational() const SIP_HOLDGIL; + + /** + * Returns TRUE if this curve represents a poly-Bézier curve. + * A poly-Bézier is a degree 3 NURBS with (n-1) divisible by 3, + * where n is the number of control points. + */ + bool isPolyBezier() const SIP_HOLDGIL; + + bool isClosed() const override SIP_HOLDGIL; + bool isClosed2D() const override SIP_HOLDGIL; + + // QgsCurve interface + QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const override SIP_FACTORY; + void draw( QPainter &p ) const override; + void drawAsPolygon( QPainter &p ) const override; + QgsPoint endPoint() const override SIP_HOLDGIL; + bool equals( const QgsCurve &other ) const override; + int indexOf( const QgsPoint &point ) const override; + QgsPoint *interpolatePoint( double distance ) const override SIP_FACTORY; + int numPoints() const override SIP_HOLDGIL; + bool pointAt( int node, QgsPoint &point SIP_OUT, Qgis::VertexType &type SIP_OUT ) const override; + void points( QgsPointSequence &pts SIP_OUT ) const override; + QgsCurve *reversed() const override SIP_FACTORY; + void scroll( int firstVertexIndex ) override; + std::tuple, std::unique_ptr> splitCurveAtVertex( int index ) const override SIP_SKIP; + QgsPoint startPoint() const override SIP_HOLDGIL; + void sumUpArea( double &sum SIP_OUT ) const override; + void sumUpArea3D( double &sum SIP_OUT ) const override; + double xAt( int index ) const override; + double yAt( int index ) const override; + double zAt( int index ) const override; + double mAt( int index ) const override; + + QPolygonF asQPolygonF() const override; + + void addToPainterPath( QPainterPath &path ) const override; + QgsCurve *curveSubstring( double startDistance, double endDistance ) const override SIP_FACTORY; + double length() const override SIP_HOLDGIL; + double segmentLength( QgsVertexId startVertex ) const override; + double distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const override; + QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const override SIP_FACTORY; + QgsAbstractGeometry *simplifyByDistance( double tolerance ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits::epsilon(), bool useZValues = false ) override; + double vertexAngle( QgsVertexId vertex ) const override; + void swapXy() override; + bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = nullptr ) override; + QgsAbstractGeometry *createEmptyWithSameType() const override SIP_FACTORY; + double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt SIP_OUT, QgsVertexId &vertexAfter SIP_OUT, int *leftOf SIP_OUT = nullptr, double epsilon = 4 * std::numeric_limits::epsilon() ) const override; + void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ) override SIP_THROW( QgsCsException ); + void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override; + QgsRectangle boundingBox() const override; + QgsBox3D boundingBox3D() const override; + bool moveVertex( QgsVertexId position, const QgsPoint &newPos ) override; + bool insertVertex( QgsVertexId position, const QgsPoint &vertex ) override; + int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const override; + QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const override; + QString asWkt( int precision = 17 ) const override; + QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override; + QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override; + json asJsonObject( int precision = 17 ) const override SIP_SKIP; + QString asKml( int precision = 17 ) const override; + int dimension() const override SIP_HOLDGIL; + bool isEmpty() const override SIP_HOLDGIL; + void clear() override; + bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL; + bool boundingBoxIntersects( const QgsBox3D &box3d ) const override SIP_HOLDGIL; + QgsPoint centroid() const override; + + // QgsAbstractGeometry interface + bool addZValue( double zValue = 0 ) override; + bool addMValue( double mValue = 0 ) override; + bool dropZValue() override; + bool dropMValue() override; + bool deleteVertex( QgsVertexId position ) override; +#ifndef SIP_RUN + void filterVertices( const std::function &filter ) override; +#endif + bool fromWkb( QgsConstWkbPtr &wkb ) override; + bool fromWkt( const QString &wkt ) override; + bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const override SIP_HOLDGIL; + bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const override SIP_HOLDGIL; + QString geometryType() const override SIP_HOLDGIL; + bool hasCurvedSegments() const override SIP_HOLDGIL; + int partCount() const override SIP_HOLDGIL; + QgsCurve *toCurveType() const override; + QgsPoint vertexAt( QgsVertexId id ) const override; + int vertexCount( int part = 0, int ring = 0 ) const override SIP_HOLDGIL; + int vertexNumberFromVertexId( QgsVertexId id ) const override; + bool isValid( QString &error SIP_OUT, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const override; + + /** + * Returns the degree of the NURBS curve. + */ + int degree() const SIP_HOLDGIL { return mDegree; } + + /** + * Sets the degree of the NURBS curve. + * \param degree curve degree (typically 1-3) + */ + void setDegree( int degree ) + { + mDegree = degree; + clearCache(); + } + + /** + * Returns the control points of the NURBS curve. + */ + QVector controlPoints() const SIP_HOLDGIL { return mControlPoints; } + + /** + * Sets the control points of the NURBS curve. + * \param points control points + */ + void setControlPoints( const QVector &points ) + { + mControlPoints = points; + clearCache(); + } + + /** + * Returns the knot vector of the NURBS curve. + */ + QVector knots() const SIP_HOLDGIL { return mKnots; } + + /** + * Sets the knot vector of the NURBS curve. + * \param knots knot vector (must have size = control points count + degree + 1, values must be non-decreasing) + */ + void setKnots( const QVector &knots ) + { + mKnots = knots; + clearCache(); + } + + /** + * Returns the weight vector of the NURBS curve. + */ + QVector weights() const SIP_HOLDGIL { return mWeights; } + + /** + * Sets the weight vector of the NURBS curve. + * \param weights weight vector (same size as control points) + */ + void setWeights( const QVector &weights ) + { + mWeights = weights; + clearCache(); + } + +#ifndef SIP_RUN + + /** + * Returns the weight at the specified control point \a index. + * Returns 1.0 if index is out of range. + * \since QGIS 4.0 + */ + double weight( int index ) const SIP_HOLDGIL; + + /** + * Sets the \a weight at the specified control point \a index. + * Weight must be positive (> 0). + * \returns TRUE if successful, FALSE if index is out of range or weight is invalid. + * \since QGIS 4.0 + */ + bool setWeight( int index, double weight ); +#else + + /** + * Returns the weight at the specified control point \a index. + * + * \throws IndexError if no control point with the specified index exists. + * \since QGIS 4.0 + */ + double weight( int index ) const SIP_HOLDGIL; + % MethodCode + const int count = sipCpp->controlPoints().size(); + if ( a0 < 0 || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + sipRes = sipCpp->weight( a0 ); + } + % End + + /** + * Sets the \a weight at the specified control point \a index. + * Weight must be positive (> 0). + * + * \throws IndexError if no control point with the specified index exists. + * \throws ValueError if weight is not positive. + * \since QGIS 4.0 + */ + void setWeight( int index, double weight ); + % MethodCode + const int count = sipCpp->controlPoints().size(); + if ( a0 < 0 || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else if ( a1 <= 0 ) + { + PyErr_SetString( PyExc_ValueError, "Weight must be positive (> 0)" ); + sipIsErr = 1; + } + else + { + sipCpp->setWeight( a0, a1 ); + } + % End +#endif + + /** + * Cast the \a geom to a QgsNurbsCurve. + * Should be used by qgsgeometry_cast( geometry ). + * \note Not available in Python. + */ + inline static const QgsNurbsCurve *cast( const QgsAbstractGeometry *geom ) SIP_SKIP // cppcheck-suppress duplInheritedMember + { + if ( geom && geom->geometryType() == "NurbsCurve"_L1 ) + return static_cast( geom ); + return nullptr; + } + + /** + * Cast the \a geom to a QgsNurbsCurve. + * Should be used by qgsgeometry_cast( geometry ). + * \note Not available in Python. + */ + inline static QgsNurbsCurve *cast( QgsAbstractGeometry *geom ) SIP_SKIP // cppcheck-suppress duplInheritedMember + { + if ( geom && geom->geometryType() == "NurbsCurve"_L1 ) + return static_cast( geom ); + return nullptr; + } + + protected: + void clearCache() const override; + int compareToSameClass( const QgsAbstractGeometry *other ) const final; + QgsBox3D calculateBoundingBox3D() const override; + + private: + /** + * Generates a uniform knot vector based on current degree and control points count. + * Clears the existing knot vector and generates a new one following the formula: + * + * - First (degree+1) knots are 0 + * - Last (degree+1) knots are 1 + * - Interior knots are uniformly spaced + */ + void generateUniformKnots(); + + QVector mControlPoints; //! Control points defining the curve shape + QVector mKnots; //! Knot vector for B-spline basis functions + QVector mWeights; //! Weight vector for rational curves + int mDegree = 0; //! Degree of the NURBS curve + mutable bool mValidityComputed = false; //! Whether validity has been computed + mutable bool mIsValid = false; //! Cached validity state +}; + +#endif // QGSNURBSCURVE_H diff --git a/src/core/geometry/qgsnurbsutils.cpp b/src/core/geometry/qgsnurbsutils.cpp new file mode 100644 index 000000000000..3696dded2bbd --- /dev/null +++ b/src/core/geometry/qgsnurbsutils.cpp @@ -0,0 +1,174 @@ +/*************************************************************************** + qgsnurbsutils.cpp + ----------------- + begin : December 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsnurbsutils.h" + +#include "qgscompoundcurve.h" +#include "qgscurvepolygon.h" +#include "qgsgeometrycollection.h" +#include "qgsnurbscurve.h" +#include "qgsvertexid.h" + +bool QgsNurbsUtils::containsNurbsCurve( const QgsAbstractGeometry *geom ) +{ + if ( !geom ) + return false; + + if ( qgsgeometry_cast( geom ) ) + return true; + + if ( const QgsGeometryCollection *gc = qgsgeometry_cast( geom ) ) + { + for ( int i = 0; i < gc->numGeometries(); ++i ) + { + if ( containsNurbsCurve( gc->geometryN( i ) ) ) + return true; + } + } + + if ( const QgsCurvePolygon *cp = qgsgeometry_cast( geom ) ) + { + if ( containsNurbsCurve( cp->exteriorRing() ) ) + return true; + for ( int i = 0; i < cp->numInteriorRings(); ++i ) + { + if ( containsNurbsCurve( cp->interiorRing( i ) ) ) + return true; + } + } + + if ( const QgsCompoundCurve *cc = qgsgeometry_cast( geom ) ) + { + for ( int i = 0; i < cc->nCurves(); ++i ) + { + if ( containsNurbsCurve( cc->curveAt( i ) ) ) + return true; + } + } + + return false; +} + +const QgsNurbsCurve *QgsNurbsUtils::extractNurbsCurve( const QgsAbstractGeometry *geom ) +{ + if ( !geom ) + return nullptr; + + if ( const QgsNurbsCurve *nurbs = qgsgeometry_cast( geom ) ) + return nurbs; + + if ( const QgsGeometryCollection *gc = qgsgeometry_cast( geom ) ) + { + for ( int i = 0; i < gc->numGeometries(); ++i ) + { + const QgsNurbsCurve *nurbs = extractNurbsCurve( gc->geometryN( i ) ); + if ( nurbs ) + return nurbs; + } + } + + if ( const QgsCurvePolygon *cp = qgsgeometry_cast( geom ) ) + { + const QgsNurbsCurve *nurbs = extractNurbsCurve( cp->exteriorRing() ); + if ( nurbs ) + return nurbs; + for ( int i = 0; i < cp->numInteriorRings(); ++i ) + { + nurbs = extractNurbsCurve( cp->interiorRing( i ) ); + if ( nurbs ) + return nurbs; + } + } + + if ( const QgsCompoundCurve *cc = qgsgeometry_cast( geom ) ) + { + for ( int i = 0; i < cc->nCurves(); ++i ) + { + const QgsNurbsCurve *nurbs = extractNurbsCurve( cc->curveAt( i ) ); + if ( nurbs ) + return nurbs; + } + } + + return nullptr; +} + +const QgsNurbsCurve *QgsNurbsUtils::findNurbsCurveForVertex( const QgsAbstractGeometry *geom, const QgsVertexId &vid, int &localIndex ) +{ + if ( !geom ) + return nullptr; + + // Direct NURBS curve + if ( const QgsNurbsCurve *nurbs = qgsgeometry_cast( geom ) ) + { + localIndex = vid.vertex; + return nurbs; + } + + // Compound curve - find the curve containing this vertex + if ( const QgsCompoundCurve *compound = qgsgeometry_cast( geom ) ) + { + int vertexOffset = 0; + for ( int i = 0; i < compound->nCurves(); ++i ) + { + const QgsCurve *curve = compound->curveAt( i ); + const int curveVertexCount = curve->numPoints(); + + // Check if vertex is in this curve (accounting for shared endpoints) + const int adjustedCount = ( i == compound->nCurves() - 1 ) ? curveVertexCount : curveVertexCount - 1; + if ( vid.vertex < vertexOffset + adjustedCount ) + { + if ( const QgsNurbsCurve *nurbs = qgsgeometry_cast( curve ) ) + { + localIndex = vid.vertex - vertexOffset; + return nurbs; + } + return nullptr; + } + vertexOffset += adjustedCount; + } + } + + // Curve polygon - check exterior and interior rings + if ( const QgsCurvePolygon *polygon = qgsgeometry_cast( geom ) ) + { + const QgsCurve *ring = nullptr; + if ( vid.ring == 0 ) + ring = polygon->exteriorRing(); + else if ( vid.ring > 0 && vid.ring <= polygon->numInteriorRings() ) + ring = polygon->interiorRing( vid.ring - 1 ); + + if ( ring ) + return findNurbsCurveForVertex( ring, QgsVertexId( 0, 0, vid.vertex ), localIndex ); + } + + // Geometry collection + if ( const QgsGeometryCollection *collection = qgsgeometry_cast( geom ) ) + { + if ( vid.part >= 0 && vid.part < collection->numGeometries() ) + { + return findNurbsCurveForVertex( collection->geometryN( vid.part ), QgsVertexId( 0, vid.ring, vid.vertex ), localIndex ); + } + } + + return nullptr; +} + +QgsNurbsCurve *QgsNurbsUtils::findNurbsCurveForVertex( QgsAbstractGeometry *geom, const QgsVertexId &vid, int &localIndex ) +{ + return const_cast( findNurbsCurveForVertex( const_cast( geom ), vid, localIndex ) ); +} diff --git a/src/core/geometry/qgsnurbsutils.h b/src/core/geometry/qgsnurbsutils.h new file mode 100644 index 000000000000..286f12e22291 --- /dev/null +++ b/src/core/geometry/qgsnurbsutils.h @@ -0,0 +1,77 @@ +/*************************************************************************** + qgsnurbsutils.h + --------------- + begin : December 2025 + copyright : (C) 2025 by Loïc Bartoletti + email : loic dot bartoletti at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSNURBSUTILS_H +#define QGSNURBSUTILS_H + +#include "qgis_core.h" +#include "qgis_sip.h" + +class QgsAbstractGeometry; +class QgsNurbsCurve; +struct QgsVertexId; + +/** + * \ingroup core + * \class QgsNurbsUtils + * \brief Utility functions for working with NURBS curves. + * \since QGIS 4.0 + */ +class CORE_EXPORT QgsNurbsUtils +{ + public: + + /** + * Returns TRUE if the \a geom contains a NURBS curve (recursively). + */ + static bool containsNurbsCurve( const QgsAbstractGeometry *geom ); + + /** + * Extracts the first NURBS curve found in the \a geom (recursively). + * Returns NULLPTR if no NURBS curve is found. + */ + static const QgsNurbsCurve *extractNurbsCurve( const QgsAbstractGeometry *geom ); + + /** + * Finds the NURBS curve containing the vertex identified by \a vid. + * + * \param geom the geometry to search in + * \param vid the vertex identifier to search for + * \param localIndex will be set to the control point index within the found NURBS curve + * \returns the NURBS curve containing the vertex, or NULLPTR if the vertex is not part of a NURBS curve + * \note Not available in Python bindings + */ + static const QgsNurbsCurve *findNurbsCurveForVertex( + const QgsAbstractGeometry *geom, + const QgsVertexId &vid, + int &localIndex ) SIP_SKIP; + + /** + * Finds the NURBS curve containing the vertex identified by \a vid. + * + * \param geom the geometry to search in + * \param vid the vertex identifier to search for + * \param localIndex will be set to the control point index within the found NURBS curve + * \returns the NURBS curve containing the vertex, or NULLPTR if the vertex is not part of a NURBS curve + */ + static QgsNurbsCurve *findNurbsCurveForVertex( + QgsAbstractGeometry *geom, + const QgsVertexId &vid, + int &localIndex SIP_OUT ); +}; + +#endif // QGSNURBSUTILS_H diff --git a/src/core/geometry/qgswkbtypes.cpp b/src/core/geometry/qgswkbtypes.cpp index b738babf3b3c..8d77829b734e 100644 --- a/src/core/geometry/qgswkbtypes.cpp +++ b/src/core/geometry/qgswkbtypes.cpp @@ -28,8 +28,7 @@ struct WkbEntry { - WkbEntry( const QString &name, bool isMultiType, Qgis::WkbType multiType, Qgis::WkbType singleType, Qgis::WkbType flatType, Qgis::GeometryType geometryType, - bool hasZ, bool hasM ) + WkbEntry( const QString &name, bool isMultiType, Qgis::WkbType multiType, Qgis::WkbType singleType, Qgis::WkbType flatType, Qgis::GeometryType geometryType, bool hasZ, bool hasM ) : mName( name ) , mIsMultiType( isMultiType ) , mMultiType( multiType ) @@ -49,7 +48,7 @@ struct WkbEntry bool mHasM; }; -typedef QMap WkbEntries; +typedef QMap WkbEntries; Q_GLOBAL_STATIC_WITH_ARGS( WkbEntries, sWkbEntries, ( { @@ -57,11 +56,11 @@ Q_GLOBAL_STATIC_WITH_ARGS( WkbEntries, sWkbEntries, ( { Qgis::WkbType::Unknown, WkbEntry( "Unknown"_L1, false, Qgis::WkbType::Unknown, Qgis::WkbType::Unknown, Qgis::WkbType::Unknown, Qgis::GeometryType::Unknown, false, false ) }, { Qgis::WkbType::NoGeometry, WkbEntry( "NoGeometry"_L1, false, Qgis::WkbType::NoGeometry, Qgis::WkbType::NoGeometry, Qgis::WkbType::NoGeometry, Qgis::GeometryType::Null, false, false ) }, //point - {Qgis::WkbType::Point, WkbEntry( "Point"_L1, false, Qgis::WkbType::MultiPoint, Qgis::WkbType::Point, Qgis::WkbType::Point, Qgis::GeometryType::Point, false, false ) }, - {Qgis::WkbType::PointZ, WkbEntry( "PointZ"_L1, false, Qgis::WkbType::MultiPointZ, Qgis::WkbType::PointZ, Qgis::WkbType::Point, Qgis::GeometryType::Point, true, false ) }, - {Qgis::WkbType::PointM, WkbEntry( "PointM"_L1, false, Qgis::WkbType::MultiPointM, Qgis::WkbType::PointM, Qgis::WkbType::Point, Qgis::GeometryType::Point, false, true ) }, - {Qgis::WkbType::PointZM, WkbEntry( "PointZM"_L1, false, Qgis::WkbType::MultiPointZM, Qgis::WkbType::PointZM, Qgis::WkbType::Point, Qgis::GeometryType::Point, true, true ) }, - {Qgis::WkbType::Point25D, WkbEntry( "Point25D"_L1, false, Qgis::WkbType::MultiPoint25D, Qgis::WkbType::Point25D, Qgis::WkbType::Point, Qgis::GeometryType::Point, true, false ) }, + { Qgis::WkbType::Point, WkbEntry( "Point"_L1, false, Qgis::WkbType::MultiPoint, Qgis::WkbType::Point, Qgis::WkbType::Point, Qgis::GeometryType::Point, false, false ) }, + { Qgis::WkbType::PointZ, WkbEntry( "PointZ"_L1, false, Qgis::WkbType::MultiPointZ, Qgis::WkbType::PointZ, Qgis::WkbType::Point, Qgis::GeometryType::Point, true, false ) }, + { Qgis::WkbType::PointM, WkbEntry( "PointM"_L1, false, Qgis::WkbType::MultiPointM, Qgis::WkbType::PointM, Qgis::WkbType::Point, Qgis::GeometryType::Point, false, true ) }, + { Qgis::WkbType::PointZM, WkbEntry( "PointZM"_L1, false, Qgis::WkbType::MultiPointZM, Qgis::WkbType::PointZM, Qgis::WkbType::Point, Qgis::GeometryType::Point, true, true ) }, + { Qgis::WkbType::Point25D, WkbEntry( "Point25D"_L1, false, Qgis::WkbType::MultiPoint25D, Qgis::WkbType::Point25D, Qgis::WkbType::Point, Qgis::GeometryType::Point, true, false ) }, //linestring { Qgis::WkbType::LineString, WkbEntry( "LineString"_L1, false, Qgis::WkbType::MultiLineString, Qgis::WkbType::LineString, Qgis::WkbType::LineString, Qgis::GeometryType::Line, false, false ) }, { Qgis::WkbType::LineStringZ, WkbEntry( "LineStringZ"_L1, false, Qgis::WkbType::MultiLineStringZ, Qgis::WkbType::LineStringZ, Qgis::WkbType::LineString, Qgis::GeometryType::Line, true, false ) }, @@ -73,6 +72,11 @@ Q_GLOBAL_STATIC_WITH_ARGS( WkbEntries, sWkbEntries, ( { Qgis::WkbType::CircularStringZ, WkbEntry( "CircularStringZ"_L1, false, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::CircularStringZ, Qgis::WkbType::CircularString, Qgis::GeometryType::Line, true, false ) }, { Qgis::WkbType::CircularStringM, WkbEntry( "CircularStringM"_L1, false, Qgis::WkbType::MultiCurveM, Qgis::WkbType::CircularStringM, Qgis::WkbType::CircularString, Qgis::GeometryType::Line, false, true ) }, { Qgis::WkbType::CircularStringZM, WkbEntry( "CircularStringZM"_L1, false, Qgis::WkbType::MultiCurveZM, Qgis::WkbType::CircularStringZM, Qgis::WkbType::CircularString, Qgis::GeometryType::Line, true, true ) }, + //nurbscurve + { Qgis::WkbType::NurbsCurve, WkbEntry( "NurbsCurve"_L1, false, Qgis::WkbType::MultiCurve, Qgis::WkbType::NurbsCurve, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, false, false ) }, + { Qgis::WkbType::NurbsCurveZ, WkbEntry( "NurbsCurveZ"_L1, false, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::NurbsCurveZ, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, true, false ) }, + { Qgis::WkbType::NurbsCurveM, WkbEntry( "NurbsCurveM"_L1, false, Qgis::WkbType::MultiCurveM, Qgis::WkbType::NurbsCurveM, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, false, true ) }, + { Qgis::WkbType::NurbsCurveZM, WkbEntry( "NurbsCurveZM"_L1, false, Qgis::WkbType::MultiCurveZM, Qgis::WkbType::NurbsCurveZM, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, true, true ) }, //compoundcurve { Qgis::WkbType::CompoundCurve, WkbEntry( "CompoundCurve"_L1, false, Qgis::WkbType::MultiCurve, Qgis::WkbType::CompoundCurve, Qgis::WkbType::CompoundCurve, Qgis::GeometryType::Line, false, false ) }, { Qgis::WkbType::CompoundCurveZ, WkbEntry( "CompoundCurveZ"_L1, false, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::CompoundCurveZ, Qgis::WkbType::CompoundCurve, Qgis::GeometryType::Line, true, false ) }, @@ -111,16 +115,16 @@ Q_GLOBAL_STATIC_WITH_ARGS( WkbEntries, sWkbEntries, ( { Qgis::WkbType::MultiPointZM, WkbEntry( "MultiPointZM"_L1, true, Qgis::WkbType::MultiPointZM, Qgis::WkbType::PointZM, Qgis::WkbType::MultiPoint, Qgis::GeometryType::Point, true, true ) }, { Qgis::WkbType::MultiPoint25D, WkbEntry( "MultiPoint25D"_L1, true, Qgis::WkbType::MultiPoint25D, Qgis::WkbType::Point25D, Qgis::WkbType::MultiPoint, Qgis::GeometryType::Point, true, false ) }, //multiline - {Qgis::WkbType::MultiLineString, WkbEntry( "MultiLineString"_L1, true, Qgis::WkbType::MultiLineString, Qgis::WkbType::LineString, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, false, false ) }, - {Qgis::WkbType::MultiLineStringZ, WkbEntry( "MultiLineStringZ"_L1, true, Qgis::WkbType::MultiLineStringZ, Qgis::WkbType::LineStringZ, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, true, false ) }, - {Qgis::WkbType::MultiLineStringM, WkbEntry( "MultiLineStringM"_L1, true, Qgis::WkbType::MultiLineStringM, Qgis::WkbType::LineStringM, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, false, true ) }, - {Qgis::WkbType::MultiLineStringZM, WkbEntry( "MultiLineStringZM"_L1, true, Qgis::WkbType::MultiLineStringZM, Qgis::WkbType::LineStringZM, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, true, true ) }, - {Qgis::WkbType::MultiLineString25D, WkbEntry( "MultiLineString25D"_L1, true, Qgis::WkbType::MultiLineString25D, Qgis::WkbType::LineString25D, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, true, false ) }, + { Qgis::WkbType::MultiLineString, WkbEntry( "MultiLineString"_L1, true, Qgis::WkbType::MultiLineString, Qgis::WkbType::LineString, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, false, false ) }, + { Qgis::WkbType::MultiLineStringZ, WkbEntry( "MultiLineStringZ"_L1, true, Qgis::WkbType::MultiLineStringZ, Qgis::WkbType::LineStringZ, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, true, false ) }, + { Qgis::WkbType::MultiLineStringM, WkbEntry( "MultiLineStringM"_L1, true, Qgis::WkbType::MultiLineStringM, Qgis::WkbType::LineStringM, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, false, true ) }, + { Qgis::WkbType::MultiLineStringZM, WkbEntry( "MultiLineStringZM"_L1, true, Qgis::WkbType::MultiLineStringZM, Qgis::WkbType::LineStringZM, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, true, true ) }, + { Qgis::WkbType::MultiLineString25D, WkbEntry( "MultiLineString25D"_L1, true, Qgis::WkbType::MultiLineString25D, Qgis::WkbType::LineString25D, Qgis::WkbType::MultiLineString, Qgis::GeometryType::Line, true, false ) }, //multicurve - {Qgis::WkbType::MultiCurve, WkbEntry( "MultiCurve"_L1, true, Qgis::WkbType::MultiCurve, Qgis::WkbType::CompoundCurve, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, false, false ) }, - {Qgis::WkbType::MultiCurveZ, WkbEntry( "MultiCurveZ"_L1, true, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::CompoundCurveZ, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, true, false ) }, - {Qgis::WkbType::MultiCurveM, WkbEntry( "MultiCurveM"_L1, true, Qgis::WkbType::MultiCurveM, Qgis::WkbType::CompoundCurveM, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, false, true ) }, - {Qgis::WkbType::MultiCurveZM, WkbEntry( "MultiCurveZM"_L1, true, Qgis::WkbType::MultiCurveZM, Qgis::WkbType::CompoundCurveZM, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, true, true ) }, + { Qgis::WkbType::MultiCurve, WkbEntry( "MultiCurve"_L1, true, Qgis::WkbType::MultiCurve, Qgis::WkbType::CompoundCurve, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, false, false ) }, + { Qgis::WkbType::MultiCurveZ, WkbEntry( "MultiCurveZ"_L1, true, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::CompoundCurveZ, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, true, false ) }, + { Qgis::WkbType::MultiCurveM, WkbEntry( "MultiCurveM"_L1, true, Qgis::WkbType::MultiCurveM, Qgis::WkbType::CompoundCurveM, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, false, true ) }, + { Qgis::WkbType::MultiCurveZM, WkbEntry( "MultiCurveZM"_L1, true, Qgis::WkbType::MultiCurveZM, Qgis::WkbType::CompoundCurveZM, Qgis::WkbType::MultiCurve, Qgis::GeometryType::Line, true, true ) }, //multipolygon { Qgis::WkbType::MultiPolygon, WkbEntry( "MultiPolygon"_L1, true, Qgis::WkbType::MultiPolygon, Qgis::WkbType::Polygon, Qgis::WkbType::MultiPolygon, Qgis::GeometryType::Polygon, false, false ) }, { Qgis::WkbType::MultiPolygonZ, WkbEntry( "MultiPolygonZ"_L1, true, Qgis::WkbType::MultiPolygonZ, Qgis::WkbType::PolygonZ, Qgis::WkbType::MultiPolygon, Qgis::GeometryType::Polygon, true, false ) }, @@ -168,81 +172,156 @@ QString QgsWkbTypes::translatedDisplayString( Qgis::WkbType type ) { switch ( type ) { - case Qgis::WkbType::Unknown: return QObject::tr( "Unknown" ); - case Qgis::WkbType::Point: return QObject::tr( "Point" ); - case Qgis::WkbType::LineString: return QObject::tr( "LineString" ); - case Qgis::WkbType::Polygon: return QObject::tr( "Polygon" ); - case Qgis::WkbType::Triangle: return QObject::tr( "Triangle" ); - case Qgis::WkbType::PolyhedralSurface: return QObject::tr( "PolyhedralSurface" ); - case Qgis::WkbType::TIN: return QObject::tr( "TIN" ); - case Qgis::WkbType::MultiPoint: return QObject::tr( "MultiPoint" ); - case Qgis::WkbType::MultiLineString: return QObject::tr( "MultiLine" ); - case Qgis::WkbType::MultiPolygon: return QObject::tr( "MultiPolygon" ); - case Qgis::WkbType::GeometryCollection: return QObject::tr( "GeometryCollection" ); - case Qgis::WkbType::CircularString: return QObject::tr( "CircularString" ); - case Qgis::WkbType::CompoundCurve: return QObject::tr( "CompoundCurve" ); - case Qgis::WkbType::CurvePolygon: return QObject::tr( "CurvePolygon" ); - case Qgis::WkbType::MultiCurve: return QObject::tr( "MultiCurve" ); - case Qgis::WkbType::MultiSurface: return QObject::tr( "MultiSurface" ); - case Qgis::WkbType::NoGeometry: return QObject::tr( "No Geometry" ); - case Qgis::WkbType::PointZ: return QObject::tr( "PointZ" ); - case Qgis::WkbType::LineStringZ: return QObject::tr( "LineStringZ" ); - case Qgis::WkbType::PolygonZ: return QObject::tr( "PolygonZ" ); - case Qgis::WkbType::TriangleZ: return QObject::tr( "TriangleZ" ); - case Qgis::WkbType::PolyhedralSurfaceZ: return QObject::tr( "PolyhedralSurfaceZ" ); - case Qgis::WkbType::TINZ: return QObject::tr( "TINZ" ); - case Qgis::WkbType::MultiPointZ: return QObject::tr( "MultiPointZ" ); - case Qgis::WkbType::MultiLineStringZ: return QObject::tr( "MultiLineZ" ); - case Qgis::WkbType::MultiPolygonZ: return QObject::tr( "MultiPolygonZ" ); - case Qgis::WkbType::GeometryCollectionZ: return QObject::tr( "GeometryCollectionZ" ); - case Qgis::WkbType::CircularStringZ: return QObject::tr( "CircularStringZ" ); - case Qgis::WkbType::CompoundCurveZ: return QObject::tr( "CompoundCurveZ" ); - case Qgis::WkbType::CurvePolygonZ: return QObject::tr( "CurvePolygonZ" ); - case Qgis::WkbType::MultiCurveZ: return QObject::tr( "MultiCurveZ" ); - case Qgis::WkbType::MultiSurfaceZ: return QObject::tr( "MultiSurfaceZ" ); - case Qgis::WkbType::PointM: return QObject::tr( "PointM" ); - case Qgis::WkbType::LineStringM: return QObject::tr( "LineStringM" ); - case Qgis::WkbType::PolygonM: return QObject::tr( "PolygonM" ); - case Qgis::WkbType::TriangleM: return QObject::tr( "TriangleM" ); - case Qgis::WkbType::PolyhedralSurfaceM: return QObject::tr( "PolyhedralSurfaceM" ); - case Qgis::WkbType::TINM: return QObject::tr( "TINM" ); - case Qgis::WkbType::MultiPointM: return QObject::tr( "MultiPointM" ); - case Qgis::WkbType::MultiLineStringM: return QObject::tr( "MultiLineStringM" ); - case Qgis::WkbType::MultiPolygonM: return QObject::tr( "MultiPolygonM" ); - case Qgis::WkbType::GeometryCollectionM: return QObject::tr( "GeometryCollectionM" ); - case Qgis::WkbType::CircularStringM: return QObject::tr( "CircularStringM" ); - case Qgis::WkbType::CompoundCurveM: return QObject::tr( "CompoundCurveM" ); - case Qgis::WkbType::CurvePolygonM: return QObject::tr( "CurvePolygonM" ); - case Qgis::WkbType::MultiCurveM: return QObject::tr( "MultiCurveM" ); - case Qgis::WkbType::MultiSurfaceM: return QObject::tr( "MultiSurfaceM" ); - case Qgis::WkbType::PointZM: return QObject::tr( "PointZM" ); - case Qgis::WkbType::LineStringZM: return QObject::tr( "LineStringZM" ); - case Qgis::WkbType::PolygonZM: return QObject::tr( "PolygonZM" ); - case Qgis::WkbType::PolyhedralSurfaceZM: return QObject::tr( "PolyhedralSurfaceZM" ); - case Qgis::WkbType::TINZM: return QObject::tr( "TINZM" ); - case Qgis::WkbType::MultiPointZM: return QObject::tr( "MultiPointZM" ); - case Qgis::WkbType::MultiLineStringZM: return QObject::tr( "MultiLineZM" ); - case Qgis::WkbType::MultiPolygonZM: return QObject::tr( "MultiPolygonZM" ); - case Qgis::WkbType::GeometryCollectionZM: return QObject::tr( "GeometryCollectionZM" ); - case Qgis::WkbType::CircularStringZM: return QObject::tr( "CircularStringZM" ); - case Qgis::WkbType::CompoundCurveZM: return QObject::tr( "CompoundCurveZM" ); - case Qgis::WkbType::CurvePolygonZM: return QObject::tr( "CurvePolygonZM" ); - case Qgis::WkbType::MultiCurveZM: return QObject::tr( "MultiCurveZM" ); - case Qgis::WkbType::MultiSurfaceZM: return QObject::tr( "MultiSurfaceZM" ); - case Qgis::WkbType::TriangleZM: return QObject::tr( "TriangleZM" ); - case Qgis::WkbType::Point25D: return QObject::tr( "Point25D" ); - case Qgis::WkbType::LineString25D: return QObject::tr( "LineString25D" ); - case Qgis::WkbType::Polygon25D: return QObject::tr( "Polygon25D" ); - case Qgis::WkbType::MultiPoint25D: return QObject::tr( "MultiPoint25D" ); - case Qgis::WkbType::MultiLineString25D: return QObject::tr( "MultiLineString25D" ); - case Qgis::WkbType::MultiPolygon25D: return QObject::tr( "MultiPolygon25D" ); + case Qgis::WkbType::Unknown: + return QObject::tr( "Unknown" ); + case Qgis::WkbType::Point: + return QObject::tr( "Point" ); + case Qgis::WkbType::LineString: + return QObject::tr( "LineString" ); + case Qgis::WkbType::Polygon: + return QObject::tr( "Polygon" ); + case Qgis::WkbType::Triangle: + return QObject::tr( "Triangle" ); + case Qgis::WkbType::PolyhedralSurface: + return QObject::tr( "PolyhedralSurface" ); + case Qgis::WkbType::TIN: + return QObject::tr( "TIN" ); + case Qgis::WkbType::MultiPoint: + return QObject::tr( "MultiPoint" ); + case Qgis::WkbType::MultiLineString: + return QObject::tr( "MultiLine" ); + case Qgis::WkbType::MultiPolygon: + return QObject::tr( "MultiPolygon" ); + case Qgis::WkbType::GeometryCollection: + return QObject::tr( "GeometryCollection" ); + case Qgis::WkbType::CircularString: + return QObject::tr( "CircularString" ); + case Qgis::WkbType::NurbsCurve: + return QObject::tr( "NurbsCurve" ); + case Qgis::WkbType::CompoundCurve: + return QObject::tr( "CompoundCurve" ); + case Qgis::WkbType::CurvePolygon: + return QObject::tr( "CurvePolygon" ); + case Qgis::WkbType::MultiCurve: + return QObject::tr( "MultiCurve" ); + case Qgis::WkbType::MultiSurface: + return QObject::tr( "MultiSurface" ); + case Qgis::WkbType::NoGeometry: + return QObject::tr( "No Geometry" ); + case Qgis::WkbType::PointZ: + return QObject::tr( "PointZ" ); + case Qgis::WkbType::LineStringZ: + return QObject::tr( "LineStringZ" ); + case Qgis::WkbType::PolygonZ: + return QObject::tr( "PolygonZ" ); + case Qgis::WkbType::TriangleZ: + return QObject::tr( "TriangleZ" ); + case Qgis::WkbType::PolyhedralSurfaceZ: + return QObject::tr( "PolyhedralSurfaceZ" ); + case Qgis::WkbType::TINZ: + return QObject::tr( "TINZ" ); + case Qgis::WkbType::MultiPointZ: + return QObject::tr( "MultiPointZ" ); + case Qgis::WkbType::MultiLineStringZ: + return QObject::tr( "MultiLineZ" ); + case Qgis::WkbType::MultiPolygonZ: + return QObject::tr( "MultiPolygonZ" ); + case Qgis::WkbType::GeometryCollectionZ: + return QObject::tr( "GeometryCollectionZ" ); + case Qgis::WkbType::CircularStringZ: + return QObject::tr( "CircularStringZ" ); + case Qgis::WkbType::NurbsCurveZ: + return QObject::tr( "NurbsCurveZ" ); + case Qgis::WkbType::CompoundCurveZ: + return QObject::tr( "CompoundCurveZ" ); + case Qgis::WkbType::CurvePolygonZ: + return QObject::tr( "CurvePolygonZ" ); + case Qgis::WkbType::MultiCurveZ: + return QObject::tr( "MultiCurveZ" ); + case Qgis::WkbType::MultiSurfaceZ: + return QObject::tr( "MultiSurfaceZ" ); + case Qgis::WkbType::PointM: + return QObject::tr( "PointM" ); + case Qgis::WkbType::LineStringM: + return QObject::tr( "LineStringM" ); + case Qgis::WkbType::PolygonM: + return QObject::tr( "PolygonM" ); + case Qgis::WkbType::TriangleM: + return QObject::tr( "TriangleM" ); + case Qgis::WkbType::PolyhedralSurfaceM: + return QObject::tr( "PolyhedralSurfaceM" ); + case Qgis::WkbType::TINM: + return QObject::tr( "TINM" ); + case Qgis::WkbType::MultiPointM: + return QObject::tr( "MultiPointM" ); + case Qgis::WkbType::MultiLineStringM: + return QObject::tr( "MultiLineStringM" ); + case Qgis::WkbType::MultiPolygonM: + return QObject::tr( "MultiPolygonM" ); + case Qgis::WkbType::GeometryCollectionM: + return QObject::tr( "GeometryCollectionM" ); + case Qgis::WkbType::CircularStringM: + return QObject::tr( "CircularStringM" ); + case Qgis::WkbType::NurbsCurveM: + return QObject::tr( "NurbsCurveM" ); + case Qgis::WkbType::CompoundCurveM: + return QObject::tr( "CompoundCurveM" ); + case Qgis::WkbType::CurvePolygonM: + return QObject::tr( "CurvePolygonM" ); + case Qgis::WkbType::MultiCurveM: + return QObject::tr( "MultiCurveM" ); + case Qgis::WkbType::MultiSurfaceM: + return QObject::tr( "MultiSurfaceM" ); + case Qgis::WkbType::PointZM: + return QObject::tr( "PointZM" ); + case Qgis::WkbType::LineStringZM: + return QObject::tr( "LineStringZM" ); + case Qgis::WkbType::PolygonZM: + return QObject::tr( "PolygonZM" ); + case Qgis::WkbType::PolyhedralSurfaceZM: + return QObject::tr( "PolyhedralSurfaceZM" ); + case Qgis::WkbType::TINZM: + return QObject::tr( "TINZM" ); + case Qgis::WkbType::MultiPointZM: + return QObject::tr( "MultiPointZM" ); + case Qgis::WkbType::MultiLineStringZM: + return QObject::tr( "MultiLineZM" ); + case Qgis::WkbType::MultiPolygonZM: + return QObject::tr( "MultiPolygonZM" ); + case Qgis::WkbType::GeometryCollectionZM: + return QObject::tr( "GeometryCollectionZM" ); + case Qgis::WkbType::CircularStringZM: + return QObject::tr( "CircularStringZM" ); + case Qgis::WkbType::NurbsCurveZM: + return QObject::tr( "NurbsCurveZM" ); + case Qgis::WkbType::CompoundCurveZM: + return QObject::tr( "CompoundCurveZM" ); + case Qgis::WkbType::CurvePolygonZM: + return QObject::tr( "CurvePolygonZM" ); + case Qgis::WkbType::MultiCurveZM: + return QObject::tr( "MultiCurveZM" ); + case Qgis::WkbType::MultiSurfaceZM: + return QObject::tr( "MultiSurfaceZM" ); + case Qgis::WkbType::TriangleZM: + return QObject::tr( "TriangleZM" ); + case Qgis::WkbType::Point25D: + return QObject::tr( "Point25D" ); + case Qgis::WkbType::LineString25D: + return QObject::tr( "LineString25D" ); + case Qgis::WkbType::Polygon25D: + return QObject::tr( "Polygon25D" ); + case Qgis::WkbType::MultiPoint25D: + return QObject::tr( "MultiPoint25D" ); + case Qgis::WkbType::MultiLineString25D: + return QObject::tr( "MultiLineString25D" ); + case Qgis::WkbType::MultiPolygon25D: + return QObject::tr( "MultiPolygon25D" ); } return QString(); } QString QgsWkbTypes::geometryDisplayString( Qgis::GeometryType type ) { - switch ( type ) { case Qgis::GeometryType::Point: @@ -258,8 +337,6 @@ QString QgsWkbTypes::geometryDisplayString( Qgis::GeometryType type ) default: return u"Invalid type"_s; } - - } /*************************************************************************** diff --git a/src/core/geometry/qgswkbtypes.h b/src/core/geometry/qgswkbtypes.h index aec862fd40ce..8a5ec595625e 100644 --- a/src/core/geometry/qgswkbtypes.h +++ b/src/core/geometry/qgswkbtypes.h @@ -133,6 +133,18 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::CircularStringZM: return Qgis::WkbType::CircularStringZM; + case Qgis::WkbType::NurbsCurve: + return Qgis::WkbType::NurbsCurve; + + case Qgis::WkbType::NurbsCurveZ: + return Qgis::WkbType::NurbsCurveZ; + + case Qgis::WkbType::NurbsCurveM: + return Qgis::WkbType::NurbsCurveM; + + case Qgis::WkbType::NurbsCurveZM: + return Qgis::WkbType::NurbsCurveZM; + case Qgis::WkbType::CompoundCurve: case Qgis::WkbType::MultiCurve: return Qgis::WkbType::CompoundCurve; @@ -306,21 +318,25 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::CompoundCurve: case Qgis::WkbType::CircularString: + case Qgis::WkbType::NurbsCurve: case Qgis::WkbType::MultiCurve: return Qgis::WkbType::MultiCurve; case Qgis::WkbType::CompoundCurveZ: case Qgis::WkbType::CircularStringZ: + case Qgis::WkbType::NurbsCurveZ: case Qgis::WkbType::MultiCurveZ: return Qgis::WkbType::MultiCurveZ; case Qgis::WkbType::CompoundCurveM: case Qgis::WkbType::CircularStringM: + case Qgis::WkbType::NurbsCurveM: case Qgis::WkbType::MultiCurveM: return Qgis::WkbType::MultiCurveM; case Qgis::WkbType::CompoundCurveZM: case Qgis::WkbType::CircularStringZM: + case Qgis::WkbType::NurbsCurveZM: case Qgis::WkbType::MultiCurveZM: return Qgis::WkbType::MultiCurveZM; @@ -451,6 +467,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::LineString: case Qgis::WkbType::CompoundCurve: case Qgis::WkbType::CircularString: + case Qgis::WkbType::NurbsCurve: return Qgis::WkbType::CompoundCurve; case Qgis::WkbType::MultiLineString: @@ -460,6 +477,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::LineStringZ: case Qgis::WkbType::CompoundCurveZ: case Qgis::WkbType::CircularStringZ: + case Qgis::WkbType::NurbsCurveZ: case Qgis::WkbType::LineString25D: return Qgis::WkbType::CompoundCurveZ; @@ -471,6 +489,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::LineStringM: case Qgis::WkbType::CompoundCurveM: case Qgis::WkbType::CircularStringM: + case Qgis::WkbType::NurbsCurveM: return Qgis::WkbType::CompoundCurveM; case Qgis::WkbType::MultiLineStringM: @@ -480,6 +499,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::LineStringZM: case Qgis::WkbType::CompoundCurveZM: case Qgis::WkbType::CircularStringZM: + case Qgis::WkbType::NurbsCurveZM: return Qgis::WkbType::CompoundCurveZM; case Qgis::WkbType::MultiLineStringZM: @@ -557,18 +577,22 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::CircularString: case Qgis::WkbType::CompoundCurve: + case Qgis::WkbType::NurbsCurve: return Qgis::WkbType::LineString; case Qgis::WkbType::CircularStringM: case Qgis::WkbType::CompoundCurveM: + case Qgis::WkbType::NurbsCurveM: return Qgis::WkbType::LineStringM; case Qgis::WkbType::CircularStringZ: case Qgis::WkbType::CompoundCurveZ: + case Qgis::WkbType::NurbsCurveZ: return Qgis::WkbType::LineStringZ; case Qgis::WkbType::CircularStringZM: case Qgis::WkbType::CompoundCurveZM: + case Qgis::WkbType::NurbsCurveZM: return Qgis::WkbType::LineStringZM; case Qgis::WkbType::MultiCurve: @@ -735,6 +759,12 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::CircularStringZM: return Qgis::WkbType::CircularString; + case Qgis::WkbType::NurbsCurve: + case Qgis::WkbType::NurbsCurveZ: + case Qgis::WkbType::NurbsCurveM: + case Qgis::WkbType::NurbsCurveZM: + return Qgis::WkbType::NurbsCurve; + case Qgis::WkbType::CompoundCurve: case Qgis::WkbType::CompoundCurveZ: case Qgis::WkbType::CompoundCurveM: @@ -821,6 +851,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::Triangle: case Qgis::WkbType::CircularString: case Qgis::WkbType::CompoundCurve: + case Qgis::WkbType::NurbsCurve: case Qgis::WkbType::CurvePolygon: case Qgis::WkbType::PolyhedralSurface: case Qgis::WkbType::TIN: @@ -831,6 +862,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::TriangleZ: case Qgis::WkbType::CircularStringZ: case Qgis::WkbType::CompoundCurveZ: + case Qgis::WkbType::NurbsCurveZ: case Qgis::WkbType::CurvePolygonZ: case Qgis::WkbType::PolyhedralSurfaceZ: case Qgis::WkbType::TINZ: @@ -840,6 +872,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::TriangleM: case Qgis::WkbType::CircularStringM: case Qgis::WkbType::CompoundCurveM: + case Qgis::WkbType::NurbsCurveM: case Qgis::WkbType::CurvePolygonM: case Qgis::WkbType::PolyhedralSurfaceM: case Qgis::WkbType::TINM: @@ -849,6 +882,7 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::TriangleZM: case Qgis::WkbType::CircularStringZM: case Qgis::WkbType::CompoundCurveZM: + case Qgis::WkbType::NurbsCurveZM: case Qgis::WkbType::CurvePolygonZM: case Qgis::WkbType::PolyhedralSurfaceZM: case Qgis::WkbType::TINZM: @@ -872,6 +906,7 @@ class CORE_EXPORT QgsWkbTypes { case Qgis::WkbType::CircularString: case Qgis::WkbType::CompoundCurve: + case Qgis::WkbType::NurbsCurve: case Qgis::WkbType::CurvePolygon: case Qgis::WkbType::MultiCurve: case Qgis::WkbType::MultiSurface: @@ -957,15 +992,19 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::MultiLineString25D: case Qgis::WkbType::CircularString: case Qgis::WkbType::CompoundCurve: + case Qgis::WkbType::NurbsCurve: case Qgis::WkbType::MultiCurve: case Qgis::WkbType::CircularStringZ: case Qgis::WkbType::CompoundCurveZ: + case Qgis::WkbType::NurbsCurveZ: case Qgis::WkbType::MultiCurveZ: case Qgis::WkbType::CircularStringM: case Qgis::WkbType::CompoundCurveM: + case Qgis::WkbType::NurbsCurveM: case Qgis::WkbType::MultiCurveM: case Qgis::WkbType::CircularStringZM: case Qgis::WkbType::CompoundCurveZM: + case Qgis::WkbType::NurbsCurveZM: case Qgis::WkbType::MultiCurveZM: return Qgis::GeometryType::Line; @@ -1075,6 +1114,8 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::CurvePolygonZM: case Qgis::WkbType::MultiCurveZM: case Qgis::WkbType::MultiSurfaceZM: + case Qgis::WkbType::NurbsCurveZ: + case Qgis::WkbType::NurbsCurveZM: case Qgis::WkbType::Point25D: case Qgis::WkbType::LineString25D: case Qgis::WkbType::Polygon25D: @@ -1129,6 +1170,8 @@ class CORE_EXPORT QgsWkbTypes case Qgis::WkbType::CurvePolygonZM: case Qgis::WkbType::MultiCurveZM: case Qgis::WkbType::MultiSurfaceZM: + case Qgis::WkbType::NurbsCurveM: + case Qgis::WkbType::NurbsCurveZM: return true; default: diff --git a/src/core/providers/ogr/qgsogrproviderutils.cpp b/src/core/providers/ogr/qgsogrproviderutils.cpp index 3e3f285f27f7..03bd55da302e 100644 --- a/src/core/providers/ogr/qgsogrproviderutils.cpp +++ b/src/core/providers/ogr/qgsogrproviderutils.cpp @@ -1796,6 +1796,16 @@ OGRwkbGeometryType QgsOgrProviderUtils::ogrTypeFromQgisType( Qgis::WkbType type case Qgis::WkbType::CircularStringZM: return wkbCircularStringZM; + // NURBS curves are converted to CompoundCurve for OGR compatibility + case Qgis::WkbType::NurbsCurve: + return wkbCompoundCurve; + case Qgis::WkbType::NurbsCurveZ: + return wkbCompoundCurveZ; + case Qgis::WkbType::NurbsCurveM: + return wkbCompoundCurveM; + case Qgis::WkbType::NurbsCurveZM: + return wkbCompoundCurveZM; + case Qgis::WkbType::CompoundCurve: return wkbCompoundCurve; case Qgis::WkbType::CompoundCurveZ: diff --git a/src/core/qgis.h b/src/core/qgis.h index a1ad48c8fea2..7a921e5cb252 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -294,6 +294,7 @@ class CORE_EXPORT Qgis MultiSurface = 12, //!< MultiSurface PolyhedralSurface = 15, //!< PolyhedralSurface \since QGIS 3.40 TIN = 16, //!< TIN \since QGIS 3.40 + NurbsCurve = 21, //!< NurbsCurve \since QGIS 4.0 NoGeometry = 100, //!< No geometry PointZ = 1001, //!< PointZ LineStringZ = 1002, //!< LineStringZ @@ -310,6 +311,7 @@ class CORE_EXPORT Qgis MultiSurfaceZ = 1012, //!< MultiSurfaceZ PolyhedralSurfaceZ = 1015, //!< PolyhedralSurfaceZ TINZ = 1016, //!< TINZ + NurbsCurveZ = 1021, //!< NurbsCurveZ \since QGIS 4.0 PointM = 2001, //!< PointM LineStringM = 2002, //!< LineStringM PolygonM = 2003, //!< PolygonM @@ -325,6 +327,7 @@ class CORE_EXPORT Qgis MultiSurfaceM = 2012, //!< MultiSurfaceM PolyhedralSurfaceM = 2015, //!< PolyhedralSurfaceM TINM = 2016, //!< TINM + NurbsCurveM = 2021, //!< NurbsCurveM \since QGIS 4.0 PointZM = 3001, //!< PointZM LineStringZM = 3002, //!< LineStringZM PolygonZM = 3003, //!< PolygonZM @@ -340,6 +343,7 @@ class CORE_EXPORT Qgis PolyhedralSurfaceZM = 3015, //!< PolyhedralSurfaceM TINZM = 3016, //!< TINZM TriangleZM = 3017, //!< TriangleZM + NurbsCurveZM = 3021, //!< NurbsCurveZM \since QGIS 4.0 Point25D = 0x80000001, //!< Point25D LineString25D, //!< LineString25D Polygon25D, //!< Polygon25D @@ -3083,6 +3087,7 @@ class CORE_EXPORT Qgis { Segment SIP_MONKEYPATCH_COMPAT_NAME( SegmentVertex ) = 1, //!< The actual start or end point of a segment Curve SIP_MONKEYPATCH_COMPAT_NAME( CurveVertex ) = 2, //!< An intermediate point on a segment defining the curvature of the segment + ControlPoint SIP_MONKEYPATCH_COMPAT_NAME( ControlPointVertex ) = 3, //!< A NURBS control point (does not lie on the curve) \since QGIS 4.0 }; Q_ENUM( VertexType ) diff --git a/src/core/qgspointlocator.cpp b/src/core/qgspointlocator.cpp index 504bb3ae65c2..51061067c215 100644 --- a/src/core/qgspointlocator.cpp +++ b/src/core/qgspointlocator.cpp @@ -358,10 +358,8 @@ class QgsPointLocator_VisitorNearestLineEndpoint : public IVisitor QgsPointLocator::MatchFilter *mFilter = nullptr; }; - //////////////////////////////////////////////////////////////////////////// - /** * \ingroup core * \brief Helper class used when traversing the index looking for edges - builds a list of matches. diff --git a/src/providers/oracle/qgsoracleprovider.cpp b/src/providers/oracle/qgsoracleprovider.cpp index 042d7a8a9d76..1f368cfb2532 100644 --- a/src/providers/oracle/qgsoracleprovider.cpp +++ b/src/providers/oracle/qgsoracleprovider.cpp @@ -2332,6 +2332,11 @@ void QgsOracleProvider::appendGeomParam( const QgsGeometry &geom, QSqlQuery &qry case Qgis::WkbType::TriangleZM: case Qgis::WkbType::Unknown: case Qgis::WkbType::NoGeometry: + // TOOD: NURBS should be supported, but not yet tested on ORACLE + case Qgis::WkbType::NurbsCurve: + case Qgis::WkbType::NurbsCurveZ: + case Qgis::WkbType::NurbsCurveM: + case Qgis::WkbType::NurbsCurveZM: g.isNull = true; break; diff --git a/tests/src/core/geometry/CMakeLists.txt b/tests/src/core/geometry/CMakeLists.txt index 88be357f5538..a7dda5a07ffe 100644 --- a/tests/src/core/geometry/CMakeLists.txt +++ b/tests/src/core/geometry/CMakeLists.txt @@ -25,6 +25,7 @@ set(TESTS testqgsmultipoint.cpp testqgsmultipolygon.cpp testqgsmultisurface.cpp + testqgsnurbscurve.cpp testqgspoint.cpp testqgspointxy.cpp testqgspolygon.cpp diff --git a/tests/src/core/geometry/testqgsgeometryutils.cpp b/tests/src/core/geometry/testqgsgeometryutils.cpp index 876525cabbae..917e1c76c4a4 100644 --- a/tests/src/core/geometry/testqgsgeometryutils.cpp +++ b/tests/src/core/geometry/testqgsgeometryutils.cpp @@ -79,6 +79,7 @@ class TestQgsGeometryUtils : public QObject void testPointOnLineWithDistance(); void testPointFractionAlongLine(); void interpolatePointOnArc(); + void testInterpolatePointOnCubicBezier(); void testSegmentizeArcHalfCircle(); void testSegmentizeArcHalfCircleOtherDirection(); void testSegmentizeArcFullCircle(); @@ -2066,5 +2067,42 @@ void TestQgsGeometryUtils::testCheckWeaklyFor3DPlane() QVERIFY( !QgsGeometryUtils::checkWeaklyFor3DPlane( &Line3DNoPlane, pt1, pt2, pt3 ) ); } +void TestQgsGeometryUtils::testInterpolatePointOnCubicBezier() +{ + // 2D + QCOMPARE( QgsGeometryUtils::interpolatePointOnCubicBezier( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, -1 ), QgsPoint( 3, 0 ), 0 ), QgsPoint( 0, 0 ) ); + QCOMPARE( QgsGeometryUtils::interpolatePointOnCubicBezier( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, -1 ), QgsPoint( 3, 0 ), 1 ), QgsPoint( 3, 0 ) ); + QgsPoint p = QgsGeometryUtils::interpolatePointOnCubicBezier( + QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, -1 ), QgsPoint( 3, 0 ), 0.5 + ); + QVERIFY( qgsDoubleNear( p.x(), 1.5 ) ); + QVERIFY( qgsDoubleNear( p.y(), 0.0 ) ); + + // With Z + p = QgsGeometryUtils::interpolatePointOnCubicBezier( + QgsPoint( 0, 0, 10 ), QgsPoint( 1, 1, 12 ), QgsPoint( 2, -1, 14 ), QgsPoint( 3, 0, 16 ), 0.5 + ); + QVERIFY( qgsDoubleNear( p.x(), 1.5 ) ); + QVERIFY( qgsDoubleNear( p.y(), 0.0 ) ); + QVERIFY( qgsDoubleNear( p.z(), 13.0 ) ); + + // With M + p = QgsGeometryUtils::interpolatePointOnCubicBezier( + QgsPoint( Qgis::WkbType::PointM, 0, 0, 0, 20 ), QgsPoint( Qgis::WkbType::PointM, 1, 1, 0, 22 ), QgsPoint( Qgis::WkbType::PointM, 2, -1, 0, 24 ), QgsPoint( Qgis::WkbType::PointM, 3, 0, 0, 26 ), 0.5 + ); + QVERIFY( qgsDoubleNear( p.x(), 1.5 ) ); + QVERIFY( qgsDoubleNear( p.y(), 0.0 ) ); + QVERIFY( qgsDoubleNear( p.m(), 23.0 ) ); + + // With Z and M + p = QgsGeometryUtils::interpolatePointOnCubicBezier( + QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 20 ), QgsPoint( Qgis::WkbType::PointZM, 1, 1, 12, 22 ), QgsPoint( Qgis::WkbType::PointZM, 2, -1, 14, 24 ), QgsPoint( Qgis::WkbType::PointZM, 3, 0, 16, 26 ), 0.5 + ); + QVERIFY( qgsDoubleNear( p.x(), 1.5 ) ); + QVERIFY( qgsDoubleNear( p.y(), 0.0 ) ); + QVERIFY( qgsDoubleNear( p.z(), 13.0 ) ); + QVERIFY( qgsDoubleNear( p.m(), 23.0 ) ); +} + QGSTEST_MAIN( TestQgsGeometryUtils ) #include "testqgsgeometryutils.moc" diff --git a/tests/src/core/geometry/testqgsgeometryutilsbase.cpp b/tests/src/core/geometry/testqgsgeometryutilsbase.cpp index 3e1e2506a422..8df546a6c808 100644 --- a/tests/src/core/geometry/testqgsgeometryutilsbase.cpp +++ b/tests/src/core/geometry/testqgsgeometryutilsbase.cpp @@ -32,6 +32,7 @@ class TestQgsGeometryUtilsBase : public QObject void testCreateFilletBase_data(); void testCreateFilletBase(); void testPointsAreCollinear(); + void testInterpolatePointOnCubicBezier(); }; void TestQgsGeometryUtilsBase::testFuzzyEqual() @@ -426,5 +427,92 @@ void TestQgsGeometryUtilsBase::testPointsAreCollinear() QVERIFY( !QgsGeometryUtilsBase::points3DAreCollinear( 1, 0, 0, 0, 0, 0, 0, 1, 1, 0.00001 ) ); } +void TestQgsGeometryUtilsBase::testInterpolatePointOnCubicBezier() +{ + double outX, outY, outZ, outM; + + // + // 2D + // + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + 0, 0, 0, 0, + 1, 1, 0, 0, + 2, -1, 0, 0, + 3, 0, 0, 0, + 0, false, false, + outX, outY, outZ, outM + ); + QVERIFY( qgsDoubleNear( outX, 0.0 ) ); + QVERIFY( qgsDoubleNear( outY, 0.0 ) ); + + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + 0, 0, 0, 0, + 1, 1, 0, 0, + 2, -1, 0, 0, + 3, 0, 0, 0, + 1, false, false, + outX, outY, outZ, outM + ); + QVERIFY( qgsDoubleNear( outX, 3.0 ) ); + QVERIFY( qgsDoubleNear( outY, 0.0 ) ); + + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + 0, 0, 0, 0, + 1, 1, 0, 0, + 2, -1, 0, 0, + 3, 0, 0, 0, + 0.5, false, false, + outX, outY, outZ, outM + ); + QVERIFY( qgsDoubleNear( outX, 1.5 ) ); + QVERIFY( qgsDoubleNear( outY, 0.0 ) ); + + // + // With Z + // + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + 0, 0, 10, 0, + 1, 1, 12, 0, + 2, -1, 14, 0, + 3, 0, 16, 0, + 0.5, true, false, + outX, outY, outZ, outM + ); + QVERIFY( qgsDoubleNear( outX, 1.5 ) ); + QVERIFY( qgsDoubleNear( outY, 0.0 ) ); + QVERIFY( qgsDoubleNear( outZ, 13.0 ) ); + + // + // With M + // + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + 0, 0, 0, 20, + 1, 1, 0, 22, + 2, -1, 0, 24, + 3, 0, 0, 26, + 0.5, false, true, + outX, outY, outZ, outM + ); + QVERIFY( qgsDoubleNear( outX, 1.5 ) ); + QVERIFY( qgsDoubleNear( outY, 0.0 ) ); + QVERIFY( qgsDoubleNear( outM, 23.0 ) ); + + // + // With Z and M + // + QgsGeometryUtilsBase::interpolatePointOnCubicBezier( + 0, 0, 10, 20, + 1, 1, 12, 22, + 2, -1, 14, 24, + 3, 0, 16, 26, + 0.5, true, true, + outX, outY, outZ, outM + ); + QVERIFY( qgsDoubleNear( outX, 1.5 ) ); + QVERIFY( qgsDoubleNear( outY, 0.0 ) ); + QVERIFY( qgsDoubleNear( outZ, 13.0 ) ); + QVERIFY( qgsDoubleNear( outM, 23.0 ) ); +} + QGSTEST_MAIN( TestQgsGeometryUtilsBase ) #include "testqgsgeometryutilsbase.moc" diff --git a/tests/src/core/geometry/testqgsnurbscurve.cpp b/tests/src/core/geometry/testqgsnurbscurve.cpp new file mode 100644 index 000000000000..eadbc2ba16ec --- /dev/null +++ b/tests/src/core/geometry/testqgsnurbscurve.cpp @@ -0,0 +1,849 @@ +/*************************************************************************** + testqgsnurbscurve.cpp + -------------------------------------- + Date : November 2025 + Copyright : (C) 2025 by Loïc Bartoletti + Email : loic dot bartoletti at oslandia dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include + +#include "qgscurve.h" +#include "qgsgeometry.h" +#include "qgslinestring.h" +#include "qgsnurbscurve.h" +#include "qgspoint.h" +#include "qgstest.h" +#include "qgsvertexid.h" +#include "testgeometryutils.h" + +#include +#include + +class TestQgsNurbsCurve : public QObject +{ + Q_OBJECT + private slots: + void emptyConstructor(); + void constructorWithParams(); + void constructorDegree1(); + void constructorDegree2(); + void constructorDegree3(); + void constructorDegree4(); + void constructorDegree5(); + void clone(); + void geometryType(); + void dimension(); + void properties(); + void isBSpline(); + void isRational(); + void isBezier(); + void evaluation(); + void startEndPoints(); + void numPoints(); + void isEmpty(); + void length(); + void hasCurvedSegments(); + void curveToLine(); + void reversed(); + void boundingBox(); + void equals(); + void vertexAt(); + void addZValue(); + void addMValue(); + void dropZValue(); + void dropMValue(); + void toFromWkt(); + void toFromWktZ(); + void toFromWktM(); + void toFromWktZM(); + void toFromWktWithWeights(); + void toFromWktWithKnots(); + void toFromWktEmpty(); + void toFromWktInvalid(); + void toFromWkb(); + void wkbCompatibilityWithSFCGAL(); + void asGeometry(); + void cast(); + void isValidTests(); + void weightAccessTests(); + void evaluateInvalidNurbs(); +}; + +void TestQgsNurbsCurve::emptyConstructor() +{ + QgsNurbsCurve curve; + + QVERIFY( curve.isEmpty() ); + QCOMPARE( curve.numPoints(), 0 ); + QCOMPARE( curve.vertexCount(), 0 ); + QCOMPARE( curve.degree(), 0 ); + QVERIFY( curve.controlPoints().isEmpty() ); + QVERIFY( curve.knots().isEmpty() ); + QVERIFY( curve.weights().isEmpty() ); + QVERIFY( !curve.is3D() ); + QVERIFY( !curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + QCOMPARE( curve.wktTypeStr(), QString( "NurbsCurve" ) ); + QCOMPARE( curve.geometryType(), QString( "NurbsCurve" ) ); + QCOMPARE( curve.dimension(), 1 ); +} + +void TestQgsNurbsCurve::constructorWithParams() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 3, 1 ) }; + int degree = 2; + QVector knots { 0, 0, 0, 1, 2, 2, 2 }; + QVector weights { 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QVERIFY( !curve.isEmpty() ); + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 4 ); + QCOMPARE( curve.knots().size(), 7 ); + QCOMPARE( curve.weights().size(), 4 ); + + QString error; + QVERIFY( curve.isValid( error, Qgis::GeometryValidityFlags() ) ); +} + +void TestQgsNurbsCurve::constructorDegree1() +{ + // Linear NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + int degree = 1; + QVector knots { 0, 0, 1, 1 }; + QVector weights { 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 1 ); + QCOMPARE( curve.controlPoints().size(), 2 ); + QCOMPARE( curve.knots().size(), 4 ); +} + +void TestQgsNurbsCurve::constructorDegree2() +{ + // Quadratic NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + int degree = 2; + QVector knots { 0, 0, 0, 1, 1, 1 }; + QVector weights { 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 3 ); + QCOMPARE( curve.knots().size(), 6 ); +} + +void TestQgsNurbsCurve::constructorDegree3() +{ + // Cubic NURBS curve (Bézier) + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 3, 10 ), QgsPoint( 7, 10 ), QgsPoint( 10, 0 ) }; + int degree = 3; + QVector knots { 0, 0, 0, 0, 1, 1, 1, 1 }; + QVector weights { 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 3 ); + QCOMPARE( curve.controlPoints().size(), 4 ); + QCOMPARE( curve.knots().size(), 8 ); +} + +void TestQgsNurbsCurve::constructorDegree4() +{ + // Quartic NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 2, 8 ), QgsPoint( 5, 12 ), QgsPoint( 8, 8 ), QgsPoint( 10, 0 ) }; + int degree = 4; + QVector knots { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 }; + QVector weights { 1, 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 4 ); + QCOMPARE( curve.controlPoints().size(), 5 ); + QCOMPARE( curve.knots().size(), 10 ); +} + +void TestQgsNurbsCurve::constructorDegree5() +{ + // Quintic NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 2 ), QgsPoint( 3, 4 ), QgsPoint( 5, 6 ), QgsPoint( 7, 4 ), QgsPoint( 8, 2 ), QgsPoint( 10, 0 ) }; + int degree = 5; + QVector knots { 0, 0, 0, 0, 0, 0, 0.5, 1, 1, 1, 1, 1, 1 }; + QVector weights { 1, 1, 1, 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 5 ); + QCOMPARE( curve.controlPoints().size(), 7 ); + QCOMPARE( curve.knots().size(), 13 ); +} + +void TestQgsNurbsCurve::clone() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve original( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + std::unique_ptr cloned( original.clone() ); + + QVERIFY( cloned != nullptr ); + QCOMPARE( cloned->geometryType(), QString( "NurbsCurve" ) ); + QVERIFY( cloned.get() != &original ); + QGSCOMPARENEAR( original.length(), cloned->length(), 0.00001 ); +} + +void TestQgsNurbsCurve::geometryType() +{ + QgsNurbsCurve curve; + QCOMPARE( curve.geometryType(), QString( "NurbsCurve" ) ); +} + +void TestQgsNurbsCurve::dimension() +{ + QgsNurbsCurve curve; + QCOMPARE( curve.dimension(), 1 ); +} + +void TestQgsNurbsCurve::properties() +{ + // Test B-spline (all weights = 1) + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve bspline( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QVERIFY( bspline.isBSpline() ); + QVERIFY( !bspline.isRational() ); +} + +void TestQgsNurbsCurve::isBSpline() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + + // B-spline: all weights = 1 + QgsNurbsCurve bspline( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QVERIFY( bspline.isBSpline() ); + + // Not a B-spline: non-uniform weights + QgsNurbsCurve rational( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 2, 1 } ); + QVERIFY( !rational.isBSpline() ); +} + +void TestQgsNurbsCurve::isRational() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + + // Non-rational: all weights = 1 + QgsNurbsCurve nonRational( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QVERIFY( !nonRational.isRational() ); + + // Rational: non-uniform weights + QgsNurbsCurve rational( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 2, 1 } ); + QVERIFY( rational.isRational() ); +} + +void TestQgsNurbsCurve::isBezier() +{ + // Cubic Bézier curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 2 ), QgsPoint( 2, 2 ), QgsPoint( 3, 0 ) }; + QgsNurbsCurve bezier( controlPoints, 3, QVector { 0, 0, 0, 0, 1, 1, 1, 1 }, QVector { 1, 1, 1, 1 } ); + + QVERIFY( bezier.isBezier() ); +} + +void TestQgsNurbsCurve::evaluation() +{ + // Simple linear case (degree 1) + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve linear( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QgsPoint start = linear.evaluate( 0.0 ); + QGSCOMPARENEAR( start.x(), 0.0, 0.00001 ); + QGSCOMPARENEAR( start.y(), 0.0, 0.00001 ); + + QgsPoint mid = linear.evaluate( 0.5 ); + QGSCOMPARENEAR( mid.x(), 5.0, 0.00001 ); + QGSCOMPARENEAR( mid.y(), 5.0, 0.00001 ); + + QgsPoint end = linear.evaluate( 1.0 ); + QGSCOMPARENEAR( end.x(), 10.0, 0.00001 ); + QGSCOMPARENEAR( end.y(), 10.0, 0.00001 ); + + // Test t below 0 returns start point + QgsPoint belowZero = linear.evaluate( -0.5 ); + QGSCOMPARENEAR( belowZero.x(), 0.0, 0.00001 ); + QGSCOMPARENEAR( belowZero.y(), 0.0, 0.00001 ); + + // Test t above 1 returns end point + QgsPoint aboveOne = linear.evaluate( 1.5 ); + QGSCOMPARENEAR( aboveOne.x(), 10.0, 0.00001 ); + QGSCOMPARENEAR( aboveOne.y(), 10.0, 0.00001 ); + + // Test evaluate on empty NURBS returns empty point + QgsNurbsCurve empty; + QgsPoint emptyEval = empty.evaluate( 0.5 ); + QVERIFY( emptyEval.isEmpty() ); +} + +void TestQgsNurbsCurve::startEndPoints() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 5 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QgsPoint start = curve.startPoint(); + QgsPoint end = curve.endPoint(); + + QGSCOMPARENEAR( start.x(), 0.0, 0.00001 ); + QGSCOMPARENEAR( start.y(), 0.0, 0.00001 ); + QGSCOMPARENEAR( end.x(), 10.0, 0.00001 ); + QGSCOMPARENEAR( end.y(), 5.0, 0.00001 ); +} + +void TestQgsNurbsCurve::numPoints() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QVERIFY( curve.numPoints() > 0 ); +} + +void TestQgsNurbsCurve::isEmpty() +{ + QgsNurbsCurve empty; + QVERIFY( empty.isEmpty() ); + + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + QVERIFY( !curve.isEmpty() ); +} + +void TestQgsNurbsCurve::length() +{ + // Simple linear case: 3-4-5 triangle + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 3, 4 ) }; + QgsNurbsCurve linear( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QGSCOMPARENEAR( linear.length(), 5.0, 0.01 ); +} + +void TestQgsNurbsCurve::hasCurvedSegments() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QVERIFY( curve.hasCurvedSegments() ); +} + +void TestQgsNurbsCurve::curveToLine() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + std::unique_ptr line( curve.curveToLine() ); + + QVERIFY( line != nullptr ); + QVERIFY( line->numPoints() > 2 ); + QCOMPARE( line->startPoint().x(), curve.startPoint().x() ); + QCOMPARE( line->startPoint().y(), curve.startPoint().y() ); + QCOMPARE( line->endPoint().x(), curve.endPoint().x() ); + QCOMPARE( line->endPoint().y(), curve.endPoint().y() ); +} + +void TestQgsNurbsCurve::reversed() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 5 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve original( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + std::unique_ptr reversed( original.reversed() ); + + QCOMPARE( original.startPoint().x(), reversed->endPoint().x() ); + QCOMPARE( original.startPoint().y(), reversed->endPoint().y() ); + QCOMPARE( original.endPoint().x(), reversed->startPoint().x() ); + QCOMPARE( original.endPoint().y(), reversed->startPoint().y() ); +} + +void TestQgsNurbsCurve::boundingBox() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QgsRectangle bbox = curve.boundingBox(); + + QVERIFY( bbox.xMinimum() <= 0.0 ); + QVERIFY( bbox.xMaximum() >= 10.0 ); + QVERIFY( bbox.yMinimum() <= 0.0 ); + QVERIFY( bbox.yMaximum() >= 5.0 ); +} + +void TestQgsNurbsCurve::equals() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve1( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QgsNurbsCurve curve2( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QgsNurbsCurve curve3( QVector { QgsPoint( 0, 0 ), QgsPoint( 2, 2 ) }, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( curve1.equals( curve2 ) ); + QVERIFY( !curve1.equals( curve3 ) ); +} + +void TestQgsNurbsCurve::vertexAt() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + if ( curve.numPoints() > 0 ) + { + QgsPoint vertex = curve.vertexAt( QgsVertexId( 0, 0, 0 ) ); + QVERIFY( !vertex.isEmpty() ); + } +} + +void TestQgsNurbsCurve::addZValue() +{ + // Start with a 2D curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Add Z value + QVERIFY( curve.addZValue( 5.0 ) ); + QVERIFY( curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveZ ); + + // Check control points now have Z + QGSCOMPARENEAR( curve.controlPoints()[0].z(), 5.0, 0.00001 ); + QGSCOMPARENEAR( curve.controlPoints()[1].z(), 5.0, 0.00001 ); + + // Adding Z again should fail + QVERIFY( !curve.addZValue( 10.0 ) ); +} + +void TestQgsNurbsCurve::addMValue() +{ + // Start with a 2D curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( !curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Add M value + QVERIFY( curve.addMValue( 100.0 ) ); + QVERIFY( curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveM ); + + // Check control points now have M + QGSCOMPARENEAR( curve.controlPoints()[0].m(), 100.0, 0.00001 ); + QGSCOMPARENEAR( curve.controlPoints()[1].m(), 100.0, 0.00001 ); + + // Adding M again should fail + QVERIFY( !curve.addMValue( 200.0 ) ); +} + +void TestQgsNurbsCurve::dropZValue() +{ + // Start with a 3D curve + QVector controlPoints { QgsPoint( 0, 0, 5 ), QgsPoint( 10, 10, 15 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveZ ); + + // Drop Z value + QVERIFY( curve.dropZValue() ); + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Dropping Z again should fail + QVERIFY( !curve.dropZValue() ); +} + +void TestQgsNurbsCurve::dropMValue() +{ + // Start with an M curve + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE M(1, (0 0 10, 10 10 20))"_s ) ); + + QVERIFY( curve.isMeasure() ); + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveM ); + + // Drop M value + QVERIFY( curve.dropMValue() ); + QVERIFY( !curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Dropping M again should fail + QVERIFY( !curve.dropMValue() ); +} + +void TestQgsNurbsCurve::toFromWkt() +{ + // Basic NURBS curve + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE(2, (0 0, 5 10, 10 0))"_s ) ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 3 ); + QVERIFY( !curve.isEmpty() ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( u"NurbsCurve"_s ) ); +} + +void TestQgsNurbsCurve::toFromWktZ() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE Z(2, (0 0 0, 5 10 5, 10 0 0))"_s ) ); + + QVERIFY( curve.is3D() ); + QCOMPARE( curve.degree(), 2 ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( u"NurbsCurve Z"_s ) ); +} + +void TestQgsNurbsCurve::toFromWktM() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE M(2, (0 0 10, 5 10 20, 10 0 30))"_s ) ); + + QVERIFY( curve.isMeasure() ); + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.degree(), 2 ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( u"NurbsCurve M"_s ) ); +} + +void TestQgsNurbsCurve::toFromWktZM() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE ZM(2, (0 0 1 10, 5 10 2 20, 10 0 3 30))"_s ) ); + + QVERIFY( curve.is3D() ); + QVERIFY( curve.isMeasure() ); + QCOMPARE( curve.degree(), 2 ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( u"NurbsCurve ZM"_s ) ); +} + +void TestQgsNurbsCurve::toFromWktWithWeights() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 2, 1))"_s ) ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 3 ); + QVERIFY( curve.isRational() ); + QCOMPARE( curve.weights().size(), 3 ); +} + +void TestQgsNurbsCurve::toFromWktWithKnots() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE(2, (0 0, 3 6, 6 3, 9 0), (1, 1, 1, 1), (0, 0, 0, 0.5, 1, 1, 1))"_s ) ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 4 ); + QCOMPARE( curve.knots().size(), 7 ); +} + +void TestQgsNurbsCurve::toFromWktEmpty() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( u"NURBSCURVE EMPTY"_s ) ); + + QVERIFY( curve.isEmpty() ); +} + +void TestQgsNurbsCurve::toFromWkb() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve original( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QByteArray wkb = original.asWkb(); + QVERIFY( !wkb.isEmpty() ); + + QgsNurbsCurve restored; + QgsConstWkbPtr wkbPtr( wkb ); + restored.fromWkb( wkbPtr ); + + QCOMPARE( restored.degree(), original.degree() ); + QCOMPARE( restored.controlPoints().size(), original.controlPoints().size() ); + QVERIFY( restored.equals( original ) ); +} + +void TestQgsNurbsCurve::wkbCompatibilityWithSFCGAL() +{ + // WKB test cases compatible with SFCGAL and PostGIS + struct TestCase + { + QString wkt; + QString wkb; + }; + + QVector testCases = { + { u"NURBSCURVE(1, (0 0, 10 10))"_s, u"011500000001000000020000000100000000000000000000000000000000000100000000000024400000000000002440000400000000000000000000000000000000000000000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(1, (0 0, 5 5, 10 0))"_s, u"011500000001000000030000000100000000000000000000000000000000000100000000000014400000000000001440000100000000000024400000000000000000000500000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(1, (0 0, 10 10), (1, 1))"_s, u"011500000001000000020000000100000000000000000000000000000000000100000000000024400000000000002440000400000000000000000000000000000000000000000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(1, (0 0, 10 10), (0.5, 2.0))"_s, u"01150000000100000002000000010000000000000000000000000000000001000000000000e03f01000000000000244000000000000024400100000000000000400400000000000000000000000000000000000000000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(1, (0 0, 5 5, 10 0), (1, 1.5, 1))"_s, u"01150000000100000003000000010000000000000000000000000000000000010000000000001440000000000000144001000000000000f83f0100000000000024400000000000000000000500000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(1, (0 0, 10 10), (1, 1), (0, 0, 1, 1))"_s, u"011500000001000000020000000100000000000000000000000000000000000100000000000024400000000000002440000400000000000000000000000000000000000000000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(1, (0 0, 5 5, 10 0), (1, 1, 1), (0, 0, 0.5, 1, 1))"_s, u"011500000001000000030000000100000000000000000000000000000000000100000000000014400000000000001440000100000000000024400000000000000000000500000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 5 10, 10 0))"_s, u"0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000024400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (1 0, 1 1, 0 1))"_s, u"0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f00010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (-5 -5, 0 10, 5 -5))"_s, u"011500000002000000030000000100000000000014c000000000000014c00001000000000000000000000000000024400001000000000000144000000000000014c00006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 2 8, 8 2, 10 10))"_s, u"0115000000020000000400000001000000000000000000000000000000000001000000000000004000000000000020400001000000000000204000000000000000400001000000000000244000000000000024400007000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 1, 1))"_s, u"0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000024400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (1 0, 1 1, 0 1), (1, 0.5, 1))"_s, u"0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 3, 1))"_s, u"01150000000200000003000000010000000000000000000000000000000000010000000000001440000000000000244001000000000000084001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 5 10, 10 0), (0.5, 1, 0.5))"_s, u"01150000000200000003000000010000000000000000000000000000000001000000000000e03f010000000000001440000000000000244000010000000000002440000000000000000001000000000000e03f06000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0.5 0.25, 2.75 5.5, 5.0 0.25), (1, 1.5, 1))"_s, u"0115000000020000000300000001000000000000e03f000000000000d03f00010000000000000640000000000000164001000000000000f83f010000000000001440000000000000d03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 1, 1), (0, 0, 0, 1, 1, 1))"_s, u"0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000024400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (1 0, 1 1, 0 1), (1, 0.5, 1), (0, 0, 0, 1, 1, 1))"_s, u"0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0))"_s, u"01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 2 8, 5 12, 8 8, 10 0))"_s, u"01150000000300000005000000010000000000000000000000000000000000010000000000000040000000000000204000010000000000001440000000000000284000010000000000002040000000000000204000010000000000002440000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (5 0, 10 2.5, 10 7.5, 5 10, 0 7.5, 0 2.5, 5 0))"_s, u"011500000003000000070000000100000000000014400000000000000000000100000000000024400000000000000440000100000000000024400000000000001e40000100000000000014400000000000002440000100000000000000000000000000001e40000100000000000000000000000000000440000100000000000014400000000000000000000b0000000000000000000000000000000000000000000000000000000000000000000000000000000000d03f000000000000e03f000000000000e83f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 5 5, 10 0, 15 -5, 20 0))"_s, u"01150000000300000005000000010000000000000000000000000000000000010000000000001440000000000000144000010000000000002440000000000000000000010000000000002e4000000000000014c000010000000000003440000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 1, 1, 1))"_s, u"01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 2, 2, 1))"_s, u"011500000003000000040000000100000000000000000000000000000000000100000000000008400000000000002440010000000000000040010000000000001c400000000000002440010000000000000040010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (0.5, 1.5, 2.0, 0.8))"_s, u"01150000000300000004000000010000000000000000000000000000000001000000000000e03f010000000000000840000000000000244001000000000000f83f010000000000001c4000000000000024400100000000000000400100000000000024400000000000000000019a9999999999e93f080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 2 8, 5 12, 8 8, 10 0), (1, 3, 5, 3, 1))"_s, u"01150000000300000005000000010000000000000000000000000000000000010000000000000040000000000000204001000000000000084001000000000000144000000000000028400100000000000014400100000000000020400000000000002040010000000000000840010000000000002440000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 1, 1, 1), (0, 0, 0, 0, 1, 1, 1, 1))"_s, u"01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 2, 2, 1), (0, 0, 0, 0, 1, 1, 1, 1))"_s, u"011500000003000000040000000100000000000000000000000000000000000100000000000008400000000000002440010000000000000040010000000000001c400000000000002440010000000000000040010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 1, 1, 1), (0, 0, 0, 0, 0.3, 0.7, 1, 1))"_s, u"01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000333333333333d33f666666666666e63f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(4, (0 0, 2 8, 5 12, 8 8, 10 0))"_s, u"011500000004000000050000000100000000000000000000000000000000000100000000000000400000000000002040000100000000000014400000000000002840000100000000000020400000000000002040000100000000000024400000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(4, (0 0, 1 5, 3 8, 7 6, 10 10, 12 0))"_s, u"0115000000040000000600000001000000000000000000000000000000000001000000000000f03f000000000000144000010000000000000840000000000000204000010000000000001c400000000000001840000100000000000024400000000000002440000100000000000028400000000000000000000b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(4, (0 0, 2 8, 5 12, 8 8, 10 0), (1, 1.5, 2, 1.5, 1))"_s, u"01150000000400000005000000010000000000000000000000000000000000010000000000000040000000000000204001000000000000f83f0100000000000014400000000000002840010000000000000040010000000000002040000000000000204001000000000000f83f0100000000000024400000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(4, (0 0, 2 8, 5 12, 8 8, 10 0), (1, 1, 1, 1, 1), (0, 0, 0, 0, 0, 1, 1, 1, 1, 1))"_s, u"011500000004000000050000000100000000000000000000000000000000000100000000000000400000000000002040000100000000000014400000000000002840000100000000000020400000000000002040000100000000000024400000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(5, (0 0, 1 2, 3 4, 5 6, 7 4, 8 2, 10 0), (1, 1, 1, 1, 1, 1, 1))"_s, u"0115000000050000000700000001000000000000000000000000000000000001000000000000f03f000000000000004000010000000000000840000000000000104000010000000000001440000000000000184000010000000000001c400000000000001040000100000000000020400000000000000040000100000000000024400000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + // 3D (Z) test cases + { u"NURBSCURVE Z(1, (0 0 0, 10 10 5))"_s, u"01fd0300000100000002000000010000000000000000000000000000000000000000000000000001000000000000244000000000000024400000000000001440000400000000000000000000000000000000000000000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE Z(2, (0 0 0, 5 10 5, 10 0 0))"_s, u"01fd030000020000000300000001000000000000000000000000000000000000000000000000000100000000000014400000000000002440000000000000144000010000000000002440000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE Z(2, (0 0 0, 5 10 5, 10 0 0), (1, 2, 1))"_s, u"01fd0300000200000003000000010000000000000000000000000000000000000000000000000001000000000000144000000000000024400000000000001440010000000000000040010000000000002440000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE Z(3, (0 0 0, 3 10 3, 7 10 7, 10 0 10))"_s, u"01fd030000030000000400000001000000000000000000000000000000000000000000000000000100000000000008400000000000002440000000000000084000010000000000001c4000000000000024400000000000001c40000100000000000024400000000000000000000000000000244000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE Z(3, (0 0 0, 3 10 3, 7 10 7, 10 0 10), (1, 1.5, 1.5, 1), (0, 0, 0, 0, 1, 1, 1, 1))"_s, u"01fd030000030000000400000001000000000000000000000000000000000000000000000000000100000000000008400000000000002440000000000000084001000000000000f83f010000000000001c4000000000000024400000000000001c4001000000000000f83f0100000000000024400000000000000000000000000000244000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + // M test cases + { u"NURBSCURVE M(1, (0 0 0, 10 10 3600))"_s, u"01e5070000010000000200000001000000000000000000000000000000000000000000000000000100000000000024400000000000002440000000000020ac40000400000000000000000000000000000000000000000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE M(2, (0 0 0, 5 10 1800, 10 0 3600))"_s, u"01e50700000200000003000000010000000000000000000000000000000000000000000000000001000000000000144000000000000024400000000000209c40000100000000000024400000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE M(2, (0 0 0, 5 10 1800, 10 0 3600), (1, 2, 1))"_s, u"01e50700000200000003000000010000000000000000000000000000000000000000000000000001000000000000144000000000000024400000000000209c400100000000000000400100000000000024400000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + // ZM test cases + { u"NURBSCURVE ZM(1, (0 0 0 0, 10 10 5 3600))"_s, u"01cd0b000001000000020000000100000000000000000000000000000000000000000000000000000000000000000001000000000000244000000000000024400000000000001440000000000020ac40000400000000000000000000000000000000000000000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE ZM(2, (0 0 0 0, 5 10 5 1800, 10 0 0 3600))"_s, u"01cd0b0000020000000300000001000000000000000000000000000000000000000000000000000000000000000000010000000000001440000000000000244000000000000014400000000000209c400001000000000000244000000000000000000000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE ZM(2, (0 0 0 0, 5 10 5 1800, 10 0 0 3600), (1, 2, 1))"_s, u"01cd0b0000020000000300000001000000000000000000000000000000000000000000000000000000000000000000010000000000001440000000000000244000000000000014400000000000209c4001000000000000004001000000000000244000000000000000000000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE ZM(3, (0 0 0 0, 3 10 3 1200, 7 10 7 2400, 10 0 10 3600), (1, 1.5, 1.5, 1), (0, 0, 0, 0, 1, 1, 1, 1))"_s, u"01cd0b0000030000000400000001000000000000000000000000000000000000000000000000000000000000000000010000000000000840000000000000244000000000000008400000000000c0924001000000000000f83f010000000000001c4000000000000024400000000000001c400000000000c0a24001000000000000f83f01000000000000244000000000000000000000000000002440000000000020ac4000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f"_s }, + // Additional rational curve test cases + { u"NURBSCURVE(2, (1 0, 1 1, 0 1), (1, 0.5, 1), (0, 0, 0, 1, 1, 1))"_s, u"0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (2 0, 2 1, 0 1), (1, 0.5, 1), (0, 0, 0, 1, 1, 1))"_s, u"01150000000200000003000000010000000000000040000000000000000000010000000000000040000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 1 1, 2 0), (1, 1, 1), (0, 0, 0, 1, 1, 1))"_s, u"0115000000020000000300000001000000000000000000000000000000000001000000000000f03f000000000000f03f0001000000000000004000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 5 5, 10 0))"_s, u"0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000014400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f"_s }, + { u"NURBSCURVE(2, (0 0, 1 2, 2 1, 3 3, 4 2, 5 4, 6 3, 7 5, 8 4, 9 6, 10 5))"_s, u"0115000000020000000b00000001000000000000000000000000000000000001000000000000f03f000000000000004000010000000000000040000000000000f03f00010000000000000840000000000000084000010000000000001040000000000000004000010000000000001440000000000000104000010000000000001840000000000000084000010000000000001c400000000000001440000100000000000020400000000000001040000100000000000022400000000000001840000100000000000024400000000000001440000e0000000000000000000000000000000000000000000000000000001cc7711cc771bc3f1cc7711cc771cc3f555555555555d53f1cc7711cc771dc3f721cc7711cc7e13f555555555555e53f398ee3388ee3e83f1cc7711cc771ec3f000000000000f03f000000000000f03f000000000000f03f"_s }, + }; + + int wktSuccessCount = 0; + int wkbSuccessCount = 0; + + for ( const TestCase &testCase : testCases ) + { + // Test 1: WKT parsing + QgsGeometry geomFromWkt = QgsGeometry::fromWkt( testCase.wkt ); + if ( !geomFromWkt.isNull() ) + { + wktSuccessCount++; + + // Test 3: Convert to WKB and compare + QByteArray wkbFromWkt = geomFromWkt.asWkb(); + QString wkbHex = QString::fromLatin1( wkbFromWkt.toHex() ); + QCOMPARE( wkbHex, testCase.wkb ); + } + + // Test 2: WKB parsing + QByteArray wkbBytes = QByteArray::fromHex( testCase.wkb.toLatin1() ); + QgsGeometry geomFromWkb; + geomFromWkb.fromWkb( wkbBytes ); + if ( !geomFromWkb.isNull() ) + { + wkbSuccessCount++; + + // Test 4: Convert to WKB and compare (round-trip) + QByteArray wkbRoundTrip = geomFromWkb.asWkb(); + QString wkbHexRoundTrip = QString::fromLatin1( wkbRoundTrip.toHex() ); + QCOMPARE( wkbHexRoundTrip, testCase.wkb ); + } + } + + // All test cases should pass + QCOMPARE( wktSuccessCount, testCases.size() ); + QCOMPARE( wkbSuccessCount, testCases.size() ); +} + +void TestQgsNurbsCurve::asGeometry() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QgsGeometry geom( curve.clone() ); + + QVERIFY( !geom.isNull() ); + QVERIFY( geom.length() > 0.0 ); +} + +void TestQgsNurbsCurve::cast() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( QgsNurbsCurve::cast( &curve ) ); + + QgsLineString line; + QVERIFY( !QgsNurbsCurve::cast( &line ) ); +} + +void TestQgsNurbsCurve::toFromWktInvalid() +{ + // Test parsing invalid WKT strings + QgsNurbsCurve curve; + + // Empty/malformed WKT + // Note: "NURBSCURVE" alone is equivalent to "NURBSCURVE EMPTY" - this is valid but empty + QVERIFY( curve.fromWkt( u"NURBSCURVE"_s ) ); + QVERIFY( curve.isEmpty() ); + + // Empty parentheses should also work and create an empty curve + QVERIFY( curve.fromWkt( u"NURBSCURVE()"_s ) ); + QVERIFY( curve.isEmpty() ); + + // Invalid type should fail + QVERIFY( !curve.fromWkt( u"NOT_A_NURBS(1, (0 0, 10 10))"_s ) ); + + // Missing degree + QVERIFY( !curve.fromWkt( u"NURBSCURVE((0 0, 10 10))"_s ) ); + + // Degree without control points + QVERIFY( !curve.fromWkt( u"NURBSCURVE(2, ())"_s ) ); + + // Too few control points for degree + QVERIFY( !curve.fromWkt( u"NURBSCURVE(3, (0 0, 10 10))"_s ) ); // degree 3 requires >= 4 points + + // Mixed dimensions in WKT - the parser currently accepts mixed dimensions + // Note: This is permissive behavior - points with extra coordinates + // (like "10 10 5" in a 2D curve) will have those extra values stored + QgsNurbsCurve mixedDimsCurve; + bool parsedMixed = mixedDimsCurve.fromWkt( u"NURBSCURVE(1, (0 0, 10 10 5))"_s ); + // Parsing should succeed - the third coordinate on second point is ignored for 2D type + if ( parsedMixed ) + { + QVERIFY( !mixedDimsCurve.isEmpty() ); + } + + // Test Z type with 2D points - should add default Z + QgsNurbsCurve zCurveWith2DPoints; + QVERIFY( zCurveWith2DPoints.fromWkt( u"NURBSCURVEZ(1, (0 0, 10 10))"_s ) ); + QVERIFY( zCurveWith2DPoints.is3D() ); +} + +void TestQgsNurbsCurve::isValidTests() +{ + QString error; + + // Valid NURBS + QVector validControlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve validCurve( validControlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QVERIFY( validCurve.isValid( error, Qgis::GeometryValidityFlags() ) ); + QVERIFY( error.isEmpty() ); + + // Empty curve is invalid + QgsNurbsCurve emptyCurve; + QVERIFY( !emptyCurve.isValid( error, Qgis::GeometryValidityFlags() ) ); + + // Degree > number of control points - 1 + QgsNurbsCurve invalidDegree; + invalidDegree.setControlPoints( QVector { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) } ); + invalidDegree.setDegree( 5 ); // Degree 5 requires >= 6 control points + invalidDegree.setKnots( QVector { 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 } ); + invalidDegree.setWeights( QVector { 1, 1 } ); + QVERIFY( !invalidDegree.isValid( error, Qgis::GeometryValidityFlags() ) ); + + // Wrong knot vector size + QgsNurbsCurve wrongKnotSize; + wrongKnotSize.setControlPoints( validControlPoints ); + wrongKnotSize.setDegree( 2 ); + wrongKnotSize.setKnots( QVector { 0, 0, 1, 1 } ); // Should be 6 elements for 3 points + degree 2 + wrongKnotSize.setWeights( QVector { 1, 1, 1 } ); + QVERIFY( !wrongKnotSize.isValid( error, Qgis::GeometryValidityFlags() ) ); + + // Non-decreasing knots check (invalid) + QgsNurbsCurve decreasingKnots; + decreasingKnots.setControlPoints( validControlPoints ); + decreasingKnots.setDegree( 2 ); + decreasingKnots.setKnots( QVector { 0, 0, 1, 0.5, 1, 1 } ); // Decreasing at index 3 + decreasingKnots.setWeights( QVector { 1, 1, 1 } ); + QVERIFY( !decreasingKnots.isValid( error, Qgis::GeometryValidityFlags() ) ); + + // Wrong weights vector size + QgsNurbsCurve wrongWeightsSize; + wrongWeightsSize.setControlPoints( validControlPoints ); + wrongWeightsSize.setDegree( 2 ); + wrongWeightsSize.setKnots( QVector { 0, 0, 0, 1, 1, 1 } ); + wrongWeightsSize.setWeights( QVector { 1, 1 } ); // Should be 3 elements + QVERIFY( !wrongWeightsSize.isValid( error, Qgis::GeometryValidityFlags() ) ); +} + +void TestQgsNurbsCurve::weightAccessTests() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 2, 1 } ); + + // Valid indices + QGSCOMPARENEAR( curve.weight( 0 ), 1.0, 0.00001 ); + QGSCOMPARENEAR( curve.weight( 1 ), 2.0, 0.00001 ); + QGSCOMPARENEAR( curve.weight( 2 ), 1.0, 0.00001 ); + + // Invalid indices return 1.0 + QGSCOMPARENEAR( curve.weight( -1 ), 1.0, 0.00001 ); + QGSCOMPARENEAR( curve.weight( 3 ), 1.0, 0.00001 ); + QGSCOMPARENEAR( curve.weight( 100 ), 1.0, 0.00001 ); + + // setWeight tests + QVERIFY( curve.setWeight( 1, 3.0 ) ); + QGSCOMPARENEAR( curve.weight( 1 ), 3.0, 0.00001 ); + + // Invalid index + QVERIFY( !curve.setWeight( -1, 2.0 ) ); + QVERIFY( !curve.setWeight( 5, 2.0 ) ); + + // Invalid weight (zero or negative) + QVERIFY( !curve.setWeight( 0, 0.0 ) ); + QVERIFY( !curve.setWeight( 0, -1.0 ) ); +} + +void TestQgsNurbsCurve::evaluateInvalidNurbs() +{ + // Test evaluate on invalid NURBS returns empty point + QgsNurbsCurve invalidCurve; + invalidCurve.setControlPoints( QVector { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) } ); + invalidCurve.setDegree( 5 ); // Invalid: degree > n-1 + invalidCurve.setKnots( QVector { 0, 0, 1, 1 } ); + invalidCurve.setWeights( QVector { 1, 1 } ); + + QString error; + QVERIFY( !invalidCurve.isValid( error, Qgis::GeometryValidityFlags() ) ); + + QgsPoint result = invalidCurve.evaluate( 0.5 ); + QVERIFY( result.isEmpty() ); +} + +QGSTEST_MAIN( TestQgsNurbsCurve ) +#include "testqgsnurbscurve.moc" diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 380756e621f9..daeb756440f9 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -232,6 +232,7 @@ ADD_PYTHON_TEST(PyQgsNominatimGeocoder test_qgsnominatimgeocoder.py) ADD_PYTHON_TEST(PyQgsNullSymbolRenderer test_qgsnullsymbolrenderer.py) ADD_PYTHON_TEST(PyQgsNumericFormat test_qgsnumericformat.py) ADD_PYTHON_TEST(PyQgsOapifProvider test_provider_oapif.py) +ADD_PYTHON_TEST(PyQgsNurbsCurve test_qgsnurbscurve.py) ADD_PYTHON_TEST(PyQgsObjectCustomProperties test_qgsobjectcustomproperties.py) ADD_PYTHON_TEST(PyQgsObjectVisitor test_qgsobjectvisitor.py) ADD_PYTHON_TEST(PyQgsOgcUtils test_qgsogcutils.py) diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index 0e77d553e04f..d3a3772393ac 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -5143,6 +5143,22 @@ def testWkbTypes(self): QgsWkbTypes.singleType(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.CircularStringZM, ) + self.assertEqual( + QgsWkbTypes.singleType(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.singleType(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.singleType(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.singleType(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.NurbsCurveZM, + ) self.assertEqual( QgsWkbTypes.singleType(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.CompoundCurve, @@ -5392,6 +5408,22 @@ def testWkbTypes(self): QgsWkbTypes.multiType(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.MultiCurveZM, ) + self.assertEqual( + QgsWkbTypes.multiType(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.MultiCurve, + ) + self.assertEqual( + QgsWkbTypes.multiType(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.MultiCurveZ, + ) + self.assertEqual( + QgsWkbTypes.multiType(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.MultiCurveM, + ) + self.assertEqual( + QgsWkbTypes.multiType(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.MultiCurveZM, + ) self.assertEqual( QgsWkbTypes.multiType(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.MultiCurve, @@ -5672,6 +5704,22 @@ def testWkbTypes(self): QgsWkbTypes.promoteNonPointTypesToMulti(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.MultiCurveZM, ) + self.assertEqual( + QgsWkbTypes.promoteNonPointTypesToMulti(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.MultiCurve, + ) + self.assertEqual( + QgsWkbTypes.promoteNonPointTypesToMulti(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.MultiCurveZ, + ) + self.assertEqual( + QgsWkbTypes.promoteNonPointTypesToMulti(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.MultiCurveM, + ) + self.assertEqual( + QgsWkbTypes.promoteNonPointTypesToMulti(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.MultiCurveZM, + ) self.assertEqual( QgsWkbTypes.promoteNonPointTypesToMulti(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.MultiCurve, @@ -5950,6 +5998,22 @@ def testWkbTypes(self): QgsWkbTypes.curveType(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.CompoundCurveZM, ) + self.assertEqual( + QgsWkbTypes.curveType(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.CompoundCurve, + ) + self.assertEqual( + QgsWkbTypes.curveType(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.CompoundCurveZ, + ) + self.assertEqual( + QgsWkbTypes.curveType(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.CompoundCurveM, + ) + self.assertEqual( + QgsWkbTypes.curveType(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.CompoundCurveZM, + ) self.assertEqual( QgsWkbTypes.curveType(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.CompoundCurve, @@ -6197,6 +6261,22 @@ def testWkbTypes(self): QgsWkbTypes.linearType(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.LineStringZM, ) + self.assertEqual( + QgsWkbTypes.linearType(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.LineString, + ) + self.assertEqual( + QgsWkbTypes.linearType(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.LineStringZ, + ) + self.assertEqual( + QgsWkbTypes.linearType(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.LineStringM, + ) + self.assertEqual( + QgsWkbTypes.linearType(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.LineStringZM, + ) self.assertEqual( QgsWkbTypes.linearType(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.LineString, @@ -6413,6 +6493,22 @@ def testWkbTypes(self): QgsWkbTypes.flatType(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.CircularString, ) + self.assertEqual( + QgsWkbTypes.flatType(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.flatType(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.flatType(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.flatType(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.NurbsCurve, + ) self.assertEqual( QgsWkbTypes.flatType(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.CompoundCurve, @@ -6665,6 +6761,22 @@ def testWkbTypes(self): QgsWkbTypes.geometryType(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.GeometryType.LineGeometry, ) + self.assertEqual( + QgsWkbTypes.geometryType(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.GeometryType.LineGeometry, + ) + self.assertEqual( + QgsWkbTypes.geometryType(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.GeometryType.LineGeometry, + ) + self.assertEqual( + QgsWkbTypes.geometryType(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.GeometryType.LineGeometry, + ) + self.assertEqual( + QgsWkbTypes.geometryType(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.GeometryType.LineGeometry, + ) self.assertEqual( QgsWkbTypes.geometryType(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.GeometryType.LineGeometry, @@ -6889,6 +7001,18 @@ def testWkbTypes(self): QgsWkbTypes.displayString(QgsWkbTypes.Type.CircularStringZM), "CircularStringZM", ) + self.assertEqual( + QgsWkbTypes.displayString(QgsWkbTypes.Type.NurbsCurve), "NurbsCurve" + ) + self.assertEqual( + QgsWkbTypes.displayString(QgsWkbTypes.Type.NurbsCurveZ), "NurbsCurveZ" + ) + self.assertEqual( + QgsWkbTypes.displayString(QgsWkbTypes.Type.NurbsCurveM), "NurbsCurveM" + ) + self.assertEqual( + QgsWkbTypes.displayString(QgsWkbTypes.Type.NurbsCurveZM), "NurbsCurveZM" + ) self.assertEqual( QgsWkbTypes.displayString(QgsWkbTypes.Type.CompoundCurve), "CompoundCurve" ) @@ -7082,6 +7206,10 @@ def testWkbTypes(self): self.assertEqual( QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.CircularStringZM), 1 ) + self.assertEqual(QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.NurbsCurve), 1) + self.assertEqual(QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.NurbsCurveZ), 1) + self.assertEqual(QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.NurbsCurveM), 1) + self.assertEqual(QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.NurbsCurveZM), 1) self.assertEqual(QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.CompoundCurve), 1) self.assertEqual(QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.CompoundCurveZ), 1) self.assertEqual(QgsWkbTypes.wkbDimensions(QgsWkbTypes.Type.CompoundCurveM), 1) @@ -7184,6 +7312,10 @@ def testWkbTypes(self): self.assertEqual( QgsWkbTypes.coordDimensions(QgsWkbTypes.Type.CircularStringZM), 4 ) + self.assertEqual(QgsWkbTypes.coordDimensions(QgsWkbTypes.Type.NurbsCurve), 2) + self.assertEqual(QgsWkbTypes.coordDimensions(QgsWkbTypes.Type.NurbsCurveZ), 3) + self.assertEqual(QgsWkbTypes.coordDimensions(QgsWkbTypes.Type.NurbsCurveM), 3) + self.assertEqual(QgsWkbTypes.coordDimensions(QgsWkbTypes.Type.NurbsCurveZM), 4) self.assertEqual(QgsWkbTypes.coordDimensions(QgsWkbTypes.Type.CompoundCurve), 2) self.assertEqual( QgsWkbTypes.coordDimensions(QgsWkbTypes.Type.CompoundCurveZ), 3 @@ -7250,6 +7382,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiPolygon) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.GeometryCollection) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CircularString) + assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.NurbsCurve) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CompoundCurve) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CurvePolygon) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiCurve) @@ -7265,6 +7398,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiPolygonZ) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.GeometryCollectionZ) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CircularStringZ) + assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.NurbsCurveZ) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CompoundCurveZ) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CurvePolygonZ) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiCurveZ) @@ -7279,6 +7413,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiPolygonM) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.GeometryCollectionM) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CircularStringM) + assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.NurbsCurveM) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CompoundCurveM) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CurvePolygonM) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiCurveM) @@ -7293,6 +7428,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiPolygonZM) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.GeometryCollectionZM) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CircularStringZM) + assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.NurbsCurveZM) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CompoundCurveZM) assert QgsWkbTypes.isSingleType(QgsWkbTypes.Type.CurvePolygonZM) assert not QgsWkbTypes.isSingleType(QgsWkbTypes.Type.MultiCurveZM) @@ -7316,6 +7452,7 @@ def testWkbTypes(self): assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiPolygon) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.GeometryCollection) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CircularString) + assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.NurbsCurve) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CompoundCurve) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CurvePolygon) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiCurve) @@ -7331,6 +7468,7 @@ def testWkbTypes(self): assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiPolygonZ) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.GeometryCollectionZ) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CircularStringZ) + assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.NurbsCurveZ) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CompoundCurveZ) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CurvePolygonZ) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiCurveZ) @@ -7345,6 +7483,7 @@ def testWkbTypes(self): assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiPolygonM) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.GeometryCollectionM) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CircularStringM) + assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.NurbsCurveM) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CompoundCurveM) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CurvePolygonM) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiCurveM) @@ -7359,6 +7498,7 @@ def testWkbTypes(self): assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiPolygonZM) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.GeometryCollectionZM) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CircularStringZM) + assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.NurbsCurveZM) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CompoundCurveZM) assert not QgsWkbTypes.isMultiType(QgsWkbTypes.Type.CurvePolygonZM) assert QgsWkbTypes.isMultiType(QgsWkbTypes.Type.MultiCurveZM) @@ -7382,6 +7522,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.MultiPolygon) assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.GeometryCollection) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CircularString) + assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.NurbsCurve) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CompoundCurve) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CurvePolygon) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.MultiCurve) @@ -7397,6 +7538,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.MultiPolygonZ) assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.GeometryCollectionZ) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CircularStringZ) + assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.NurbsCurveZ) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CompoundCurveZ) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CurvePolygonZ) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.MultiCurveZ) @@ -7411,6 +7553,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.MultiPolygonM) assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.GeometryCollectionM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CircularStringM) + assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.NurbsCurveM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CompoundCurveM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CurvePolygonM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.MultiCurveM) @@ -7425,6 +7568,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.TINZM) assert not QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.GeometryCollectionZM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CircularStringZM) + assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.NurbsCurveZM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CompoundCurveZM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.CurvePolygonZM) assert QgsWkbTypes.isCurvedType(QgsWkbTypes.Type.MultiCurveZM) @@ -7448,6 +7592,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiPolygon) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.GeometryCollection) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.CircularString) + assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.NurbsCurve) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.CompoundCurve) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.CurvePolygon) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiCurve) @@ -7463,6 +7608,7 @@ def testWkbTypes(self): assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiPolygonZ) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.GeometryCollectionZ) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.CircularStringZ) + assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.NurbsCurveZ) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.CompoundCurveZ) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.CurvePolygonZ) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiCurveZ) @@ -7477,6 +7623,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiPolygonM) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.GeometryCollectionM) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.CircularStringM) + assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.NurbsCurveM) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.CompoundCurveM) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.CurvePolygonM) assert not QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiCurveM) @@ -7491,6 +7638,7 @@ def testWkbTypes(self): assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiPolygonZM) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.GeometryCollectionZM) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.CircularStringZM) + assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.NurbsCurveZM) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.CompoundCurveZM) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.CurvePolygonZM) assert QgsWkbTypes.hasZ(QgsWkbTypes.Type.MultiCurveZM) @@ -7514,6 +7662,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiPolygon) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.GeometryCollection) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.CircularString) + assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.NurbsCurve) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.CompoundCurve) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.CurvePolygon) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiCurve) @@ -7529,6 +7678,7 @@ def testWkbTypes(self): assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiPolygonZ) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.GeometryCollectionZ) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.CircularStringZ) + assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.NurbsCurveZ) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.CompoundCurveZ) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.CurvePolygonZ) assert not QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiCurveZ) @@ -7543,6 +7693,7 @@ def testWkbTypes(self): assert QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiPolygonM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.GeometryCollectionM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.CircularStringM) + assert QgsWkbTypes.hasM(QgsWkbTypes.Type.NurbsCurveM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.CompoundCurveM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.CurvePolygonM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiCurveM) @@ -7557,6 +7708,7 @@ def testWkbTypes(self): assert QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiPolygonZM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.GeometryCollectionZM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.CircularStringZM) + assert QgsWkbTypes.hasM(QgsWkbTypes.Type.NurbsCurveZM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.CompoundCurveZM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.CurvePolygonZM) assert QgsWkbTypes.hasM(QgsWkbTypes.Type.MultiCurveZM) @@ -7688,6 +7840,22 @@ def testWkbTypes(self): QgsWkbTypes.addZ(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.CircularStringZM, ) + self.assertEqual( + QgsWkbTypes.addZ(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.addZ(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.addZ(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.NurbsCurveZM, + ) + self.assertEqual( + QgsWkbTypes.addZ(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.NurbsCurveZM, + ) self.assertEqual( QgsWkbTypes.addZ(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.CompoundCurveZ, @@ -8146,6 +8314,22 @@ def testWkbTypes(self): QgsWkbTypes.addM(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.CircularStringZM, ) + self.assertEqual( + QgsWkbTypes.addM(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.addM(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.NurbsCurveZM, + ) + self.assertEqual( + QgsWkbTypes.addM(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.addM(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.NurbsCurveZM, + ) self.assertEqual( QgsWkbTypes.addM(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.CompoundCurveM, @@ -8379,6 +8563,22 @@ def testWkbTypes(self): QgsWkbTypes.dropZ(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.CircularStringM, ) + self.assertEqual( + QgsWkbTypes.dropZ(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.dropZ(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.dropZ(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.dropZ(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.NurbsCurveM, + ) self.assertEqual( QgsWkbTypes.dropZ(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.CompoundCurve, @@ -8611,6 +8811,22 @@ def testWkbTypes(self): QgsWkbTypes.dropM(QgsWkbTypes.Type.CircularStringZM), QgsWkbTypes.Type.CircularStringZ, ) + self.assertEqual( + QgsWkbTypes.dropM(QgsWkbTypes.Type.NurbsCurve), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.dropM(QgsWkbTypes.Type.NurbsCurveZ), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.dropM(QgsWkbTypes.Type.NurbsCurveM), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.dropM(QgsWkbTypes.Type.NurbsCurveZM), + QgsWkbTypes.Type.NurbsCurveZ, + ) self.assertEqual( QgsWkbTypes.dropM(QgsWkbTypes.Type.CompoundCurve), QgsWkbTypes.Type.CompoundCurve, @@ -9268,6 +9484,74 @@ def testWkbTypes(self): QgsWkbTypes.Type.CircularStringZM, ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurve, False, False), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurve, True, False), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurve, False, True), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurve, True, True), + QgsWkbTypes.Type.NurbsCurveZM, + ) + + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZ, False, False), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZ, True, False), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZ, False, True), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZ, True, True), + QgsWkbTypes.Type.NurbsCurveZM, + ) + + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveM, False, False), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveM, True, False), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveM, False, True), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveM, True, True), + QgsWkbTypes.Type.NurbsCurveZM, + ) + + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZM, False, False), + QgsWkbTypes.Type.NurbsCurve, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZM, True, False), + QgsWkbTypes.Type.NurbsCurveZ, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZM, False, True), + QgsWkbTypes.Type.NurbsCurveM, + ) + self.assertEqual( + QgsWkbTypes.zmType(QgsWkbTypes.Type.NurbsCurveZM, True, True), + QgsWkbTypes.Type.NurbsCurveZM, + ) + self.assertEqual( QgsWkbTypes.zmType(QgsWkbTypes.Type.CompoundCurve, False, False), QgsWkbTypes.Type.CompoundCurve, @@ -12819,11 +13103,11 @@ def testRoundWaves(self): ) self.assertEqual( QgsGeometry.fromWkt("LineString (1 1, 10 1)").roundWaves(5, 2).asWkt(3), - "LineString (1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.895 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.145 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.395 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.106 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.208 2.888, 9.514 2.584, 9.766 2.136, 9.937 1.592, 10 1)", + "LineString (1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.894 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.144 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.394 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.105 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.208 2.888, 9.514 2.584, 9.766 2.136, 9.937 1.592, 10 1)", ) self.assertEqual( QgsGeometry.fromWkt("LineString (1 1, 10 1)").roundWaves(8, 2).asWkt(3), - "LineString (1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.895 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.145 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.395 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.106 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.208 2.888, 9.514 2.584, 9.766 2.136, 9.937 1.592, 10 1)", + "LineString (1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.894 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.144 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.394 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.105 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.208 2.888, 9.514 2.584, 9.766 2.136, 9.937 1.592, 10 1)", ) self.assertEqual( QgsGeometry.fromWkt("LineString (1 1, 10 1)") @@ -12843,13 +13127,13 @@ def testRoundWaves(self): QgsGeometry.fromWkt("LineString (1 1, 10 1, 10 10)") .roundWaves(5, 2) .asWkt(3), - "LineString (1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.895 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.145 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.395 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.106 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.182 2.891, 9.44 2.609, 9.668 2.22, 9.885 1.792, 10.109 1.391, 10.36 1.083, 10.656 0.936, 11.015 1.016, 11.457 1.39, 12 2.125, 11.888 2.431, 11.584 2.683, 11.136 2.895, 10.592 3.079, 10 3.25, 9.408 3.421, 8.864 3.606, 8.416 3.817, 8.112 4.069, 8 4.375, 8.112 4.681, 8.416 4.933, 8.864 5.145, 9.408 5.329, 10 5.5, 10.592 5.671, 11.136 5.856, 11.584 6.067, 11.888 6.319, 12 6.625, 11.888 6.931, 11.584 7.183, 11.136 7.395, 10.592 7.579, 10 7.75, 9.408 7.921, 8.864 8.106, 8.416 8.317, 8.112 8.569, 8 8.875, 8.112 9.208, 8.416 9.514, 8.864 9.766, 9.408 9.937, 10 10)", + "LineString (1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.894 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.144 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.394 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.105 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.182 2.891, 9.44 2.609, 9.668 2.22, 9.885 1.792, 10.109 1.391, 10.36 1.083, 10.656 0.936, 11.015 1.016, 11.457 1.39, 12 2.125, 11.888 2.431, 11.584 2.683, 11.136 2.894, 10.592 3.079, 10 3.25, 9.408 3.421, 8.864 3.606, 8.416 3.817, 8.112 4.069, 8 4.375, 8.112 4.681, 8.416 4.933, 8.864 5.144, 9.408 5.329, 10 5.5, 10.592 5.671, 11.136 5.856, 11.584 6.067, 11.888 6.319, 12 6.625, 11.888 6.931, 11.584 7.183, 11.136 7.394, 10.592 7.579, 10 7.75, 9.408 7.921, 8.864 8.105, 8.416 8.317, 8.112 8.569, 8 8.875, 8.112 9.208, 8.416 9.514, 8.864 9.766, 9.408 9.937, 10 10)", ) self.assertEqual( QgsGeometry.fromWkt("MultiLineString ((1 1, 10 1),(10 10, 0 10))") .roundWaves(5, 2) .asWkt(3), - "MultiLineString ((1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.895 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.145 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.395 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.106 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.208 2.888, 9.514 2.584, 9.766 2.136, 9.937 1.592, 10 1),(10 10, 9.897 10.299, 9.78 10.592, 9.651 10.873, 9.515 11.136, 9.375 11.375, 9.235 11.584, 9.099 11.757, 8.97 11.888, 8.853 11.971, 8.75 12, 8.41 11.888, 8.13 11.584, 7.895 11.136, 7.69 10.592, 7.5 10, 7.31 9.408, 7.105 8.864, 6.87 8.416, 6.59 8.112, 6.25 8, 5.91 8.112, 5.63 8.416, 5.395 8.864, 5.19 9.408, 5 10, 4.81 10.592, 4.605 11.136, 4.37 11.584, 4.09 11.888, 3.75 12, 3.41 11.888, 3.13 11.584, 2.895 11.136, 2.69 10.592, 2.5 10, 2.31 9.408, 2.105 8.864, 1.87 8.416, 1.59 8.112, 1.25 8, 0.88 8.112, 0.54 8.416, 0.26 8.864, 0.07 9.408, 0 10))", + "MultiLineString ((1 1, 1.092 0.701, 1.198 0.408, 1.314 0.127, 1.437 -0.136, 1.563 -0.375, 1.689 -0.584, 1.811 -0.757, 1.927 -0.888, 2.033 -0.971, 2.125 -1, 2.431 -0.888, 2.683 -0.584, 2.894 -0.136, 3.079 0.408, 3.25 1, 3.421 1.592, 3.606 2.136, 3.817 2.584, 4.069 2.888, 4.375 3, 4.681 2.888, 4.933 2.584, 5.144 2.136, 5.329 1.592, 5.5 1, 5.671 0.408, 5.856 -0.136, 6.067 -0.584, 6.319 -0.888, 6.625 -1, 6.931 -0.888, 7.183 -0.584, 7.394 -0.136, 7.579 0.408, 7.75 1, 7.921 1.592, 8.105 2.136, 8.317 2.584, 8.569 2.888, 8.875 3, 9.208 2.888, 9.514 2.584, 9.766 2.136, 9.937 1.592, 10 1),(10 10, 9.897 10.299, 9.78 10.592, 9.651 10.873, 9.515 11.136, 9.375 11.375, 9.235 11.584, 9.099 11.757, 8.97 11.888, 8.853 11.971, 8.75 12, 8.41 11.888, 8.13 11.584, 7.895 11.136, 7.69 10.592, 7.5 10, 7.31 9.408, 7.105 8.864, 6.87 8.416, 6.59 8.112, 6.25 8, 5.91 8.112, 5.63 8.416, 5.395 8.864, 5.19 9.408, 5 10, 4.81 10.592, 4.605 11.136, 4.37 11.584, 4.09 11.888, 3.75 12, 3.41 11.888, 3.13 11.584, 2.895 11.136, 2.69 10.592, 2.5 10, 2.31 9.408, 2.105 8.864, 1.87 8.416, 1.59 8.112, 1.25 8, 0.88 8.112, 0.54 8.416, 0.26 8.864, 0.07 9.408, 0 10))", ) self.assertEqual( QgsGeometry.fromWkt( @@ -12857,7 +13141,7 @@ def testRoundWaves(self): ) .roundWaves(5, 0.2) .asWkt(3), - "Polygon ((1 1, 1.092 0.97, 1.198 0.941, 1.314 0.913, 1.437 0.886, 1.563 0.863, 1.689 0.842, 1.811 0.824, 1.927 0.811, 2.033 0.803, 2.125 0.8, 2.431 0.811, 2.683 0.842, 2.895 0.886, 3.079 0.941, 3.25 1, 3.421 1.059, 3.606 1.114, 3.817 1.158, 4.069 1.189, 4.375 1.2, 4.681 1.189, 4.933 1.158, 5.145 1.114, 5.329 1.059, 5.5 1, 5.671 0.941, 5.856 0.886, 6.067 0.842, 6.319 0.811, 6.625 0.8, 6.931 0.811, 7.183 0.842, 7.395 0.886, 7.579 0.941, 7.75 1, 7.921 1.059, 8.106 1.114, 8.317 1.158, 8.569 1.189, 8.875 1.2, 9.18 1.19, 9.426 1.169, 9.62 1.149, 9.77 1.144, 9.884 1.166, 9.971 1.227, 10.038 1.341, 10.093 1.52, 10.145 1.777, 10.2 2.125, 10.189 2.431, 10.158 2.683, 10.114 2.895, 10.059 3.079, 10 3.25, 9.941 3.421, 9.886 3.606, 9.842 3.817, 9.811 4.069, 9.8 4.375, 9.811 4.681, 9.842 4.933, 9.886 5.145, 9.941 5.329, 10 5.5, 10.059 5.671, 10.114 5.856, 10.158 6.067, 10.189 6.319, 10.2 6.625, 10.189 6.931, 10.158 7.183, 10.114 7.395, 10.059 7.579, 10 7.75, 9.941 7.921, 9.886 8.106, 9.842 8.317, 9.811 8.569, 9.8 8.875, 9.81 9.18, 9.831 9.426, 9.851 9.62, 9.856 9.77, 9.834 9.884, 9.773 9.971, 9.659 10.038, 9.48 10.093, 9.223 10.145, 8.875 10.2, 8.569 10.189, 8.317 10.158, 8.106 10.114, 7.921 10.059, 7.75 10, 7.579 9.941, 7.395 9.886, 7.183 9.842, 6.931 9.811, 6.625 9.8, 6.319 9.811, 6.067 9.842, 5.856 9.886, 5.671 9.941, 5.5 10, 5.329 10.059, 5.145 10.114, 4.933 10.158, 4.681 10.189, 4.375 10.2, 4.069 10.189, 3.817 10.158, 3.606 10.114, 3.421 10.059, 3.25 10, 3.079 9.941, 2.895 9.886, 2.683 9.842, 2.431 9.811, 2.125 9.8, 1.82 9.81, 1.574 9.831, 1.38 9.851, 1.23 9.856, 1.116 9.834, 1.029 9.773, 0.962 9.659, 0.907 9.48, 0.855 9.223, 0.8 8.875, 0.811 8.569, 0.842 8.317, 0.886 8.106, 0.941 7.921, 1 7.75, 1.059 7.579, 1.114 7.395, 1.158 7.183, 1.189 6.931, 1.2 6.625, 1.189 6.319, 1.158 6.067, 1.114 5.856, 1.059 5.671, 1 5.5, 0.941 5.329, 0.886 5.145, 0.842 4.933, 0.811 4.681, 0.8 4.375, 0.811 4.069, 0.842 3.817, 0.886 3.606, 0.941 3.421, 1 3.25, 1.059 3.079, 1.114 2.895, 1.158 2.683, 1.189 2.431, 1.2 2.125, 1.189 1.792, 1.158 1.486, 1.114 1.234, 1.059 1.063, 1 1),(3 4, 3.093 3.97, 3.199 3.941, 3.315 3.913, 3.438 3.886, 3.565 3.863, 3.692 3.842, 3.815 3.824, 3.931 3.811, 4.037 3.803, 4.13 3.8, 4.437 3.811, 4.691 3.842, 4.903 3.886, 5.088 3.941, 5.26 4, 5.432 4.059, 5.617 4.114, 5.83 4.158, 6.083 4.189, 6.39 4.2, 6.697 4.19, 6.945 4.165, 7.145 4.137, 7.306 4.116, 7.437 4.11, 7.548 4.131, 7.649 4.188, 7.749 4.292, 7.857 4.453, 7.984 4.68, 7.876 4.968, 7.767 5.199, 7.658 5.386, 7.547 5.545, 7.437 5.689, 7.327 5.833, 7.216 5.992, 7.107 6.179, 6.998 6.41, 6.89 6.698, 6.707 6.663, 6.546 6.654, 6.4 6.662, 6.26 6.679, 6.115 6.698, 5.959 6.712, 5.781 6.712, 5.573 6.691, 5.326 6.641, 5.032 6.555, 4.744 6.446, 4.518 6.334, 4.344 6.214, 4.21 6.084, 4.107 5.94, 4.023 5.781, 3.95 5.602, 3.876 5.401, 3.791 5.175, 3.684 4.921, 3.585 4.748, 3.451 4.548, 3.298 4.341, 3.142 4.151, 3 4))", + "Polygon ((1 1, 1.092 0.97, 1.198 0.941, 1.314 0.913, 1.437 0.886, 1.563 0.863, 1.688 0.842, 1.811 0.824, 1.927 0.811, 2.033 0.803, 2.125 0.8, 2.431 0.811, 2.683 0.842, 2.894 0.886, 3.079 0.941, 3.25 1, 3.421 1.059, 3.606 1.114, 3.817 1.158, 4.069 1.189, 4.375 1.2, 4.681 1.189, 4.933 1.158, 5.144 1.114, 5.329 1.059, 5.5 1, 5.671 0.941, 5.856 0.886, 6.067 0.842, 6.319 0.811, 6.625 0.8, 6.931 0.811, 7.183 0.842, 7.394 0.886, 7.579 0.941, 7.75 1, 7.921 1.059, 8.105 1.114, 8.317 1.158, 8.569 1.189, 8.875 1.2, 9.18 1.19, 9.426 1.169, 9.62 1.149, 9.77 1.144, 9.884 1.166, 9.971 1.227, 10.038 1.341, 10.093 1.52, 10.145 1.777, 10.2 2.125, 10.189 2.431, 10.158 2.683, 10.114 2.894, 10.059 3.079, 10 3.25, 9.941 3.421, 9.886 3.606, 9.842 3.817, 9.811 4.069, 9.8 4.375, 9.811 4.681, 9.842 4.933, 9.886 5.144, 9.941 5.329, 10 5.5, 10.059 5.671, 10.114 5.856, 10.158 6.067, 10.189 6.319, 10.2 6.625, 10.189 6.931, 10.158 7.183, 10.114 7.394, 10.059 7.579, 10 7.75, 9.941 7.921, 9.886 8.105, 9.842 8.317, 9.811 8.569, 9.8 8.875, 9.81 9.18, 9.831 9.426, 9.851 9.62, 9.856 9.77, 9.834 9.884, 9.773 9.971, 9.659 10.038, 9.48 10.093, 9.223 10.145, 8.875 10.2, 8.569 10.189, 8.317 10.158, 8.105 10.114, 7.921 10.059, 7.75 10, 7.579 9.941, 7.394 9.886, 7.183 9.842, 6.931 9.811, 6.625 9.8, 6.319 9.811, 6.067 9.842, 5.856 9.886, 5.671 9.941, 5.5 10, 5.329 10.059, 5.144 10.114, 4.933 10.158, 4.681 10.189, 4.375 10.2, 4.069 10.189, 3.817 10.158, 3.605 10.114, 3.421 10.059, 3.25 10, 3.079 9.941, 2.894 9.886, 2.683 9.842, 2.431 9.811, 2.125 9.8, 1.82 9.81, 1.574 9.831, 1.38 9.851, 1.23 9.856, 1.116 9.834, 1.029 9.773, 0.962 9.659, 0.907 9.48, 0.855 9.223, 0.8 8.875, 0.811 8.569, 0.842 8.317, 0.886 8.105, 0.941 7.921, 1 7.75, 1.059 7.579, 1.114 7.394, 1.158 7.183, 1.189 6.931, 1.2 6.625, 1.189 6.319, 1.158 6.067, 1.114 5.856, 1.059 5.671, 1 5.5, 0.941 5.329, 0.886 5.144, 0.842 4.933, 0.811 4.681, 0.8 4.375, 0.811 4.069, 0.842 3.817, 0.886 3.605, 0.941 3.421, 1 3.25, 1.059 3.079, 1.114 2.894, 1.158 2.683, 1.189 2.431, 1.2 2.125, 1.189 1.792, 1.158 1.486, 1.114 1.234, 1.059 1.063, 1 1),(3 4, 3.093 3.97, 3.199 3.941, 3.315 3.913, 3.438 3.886, 3.565 3.862, 3.692 3.842, 3.815 3.824, 3.931 3.811, 4.037 3.803, 4.13 3.8, 4.437 3.811, 4.691 3.842, 4.903 3.886, 5.088 3.941, 5.26 4, 5.432 4.059, 5.617 4.114, 5.83 4.158, 6.083 4.189, 6.39 4.2, 6.697 4.19, 6.945 4.165, 7.145 4.137, 7.306 4.116, 7.437 4.11, 7.548 4.131, 7.649 4.188, 7.749 4.292, 7.857 4.453, 7.984 4.68, 7.876 4.968, 7.767 5.199, 7.658 5.386, 7.547 5.545, 7.437 5.689, 7.327 5.833, 7.216 5.992, 7.107 6.179, 6.998 6.41, 6.89 6.698, 6.707 6.663, 6.546 6.654, 6.4 6.662, 6.26 6.679, 6.115 6.698, 5.959 6.712, 5.781 6.712, 5.573 6.691, 5.326 6.641, 5.032 6.555, 4.744 6.446, 4.518 6.334, 4.344 6.214, 4.21 6.084, 4.107 5.94, 4.023 5.781, 3.95 5.602, 3.876 5.401, 3.791 5.175, 3.684 4.921, 3.585 4.748, 3.451 4.548, 3.298 4.341, 3.142 4.151, 3 4))", ) self.assertEqual( QgsGeometry.fromWkt( @@ -12865,7 +13149,7 @@ def testRoundWaves(self): ) .roundWaves(5, 0.2) .asWkt(3), - "MultiPolygon (((1 1, 1.092 0.97, 1.198 0.941, 1.314 0.913, 1.437 0.886, 1.563 0.863, 1.689 0.842, 1.811 0.824, 1.927 0.811, 2.033 0.803, 2.125 0.8, 2.431 0.811, 2.683 0.842, 2.895 0.886, 3.079 0.941, 3.25 1, 3.421 1.059, 3.606 1.114, 3.817 1.158, 4.069 1.189, 4.375 1.2, 4.681 1.189, 4.933 1.158, 5.145 1.114, 5.329 1.059, 5.5 1, 5.671 0.941, 5.856 0.886, 6.067 0.842, 6.319 0.811, 6.625 0.8, 6.931 0.811, 7.183 0.842, 7.395 0.886, 7.579 0.941, 7.75 1, 7.921 1.059, 8.106 1.114, 8.317 1.158, 8.569 1.189, 8.875 1.2, 9.18 1.19, 9.426 1.169, 9.62 1.149, 9.77 1.144, 9.884 1.166, 9.971 1.227, 10.038 1.341, 10.093 1.52, 10.145 1.777, 10.2 2.125, 10.189 2.431, 10.158 2.683, 10.114 2.895, 10.059 3.079, 10 3.25, 9.941 3.421, 9.886 3.606, 9.842 3.817, 9.811 4.069, 9.8 4.375, 9.811 4.681, 9.842 4.933, 9.886 5.145, 9.941 5.329, 10 5.5, 10.059 5.671, 10.114 5.856, 10.158 6.067, 10.189 6.319, 10.2 6.625, 10.189 6.931, 10.158 7.183, 10.114 7.395, 10.059 7.579, 10 7.75, 9.941 7.921, 9.886 8.106, 9.842 8.317, 9.811 8.569, 9.8 8.875, 9.81 9.18, 9.831 9.426, 9.851 9.62, 9.856 9.77, 9.834 9.884, 9.773 9.971, 9.659 10.038, 9.48 10.093, 9.223 10.145, 8.875 10.2, 8.569 10.189, 8.317 10.158, 8.106 10.114, 7.921 10.059, 7.75 10, 7.579 9.941, 7.395 9.886, 7.183 9.842, 6.931 9.811, 6.625 9.8, 6.319 9.811, 6.067 9.842, 5.856 9.886, 5.671 9.941, 5.5 10, 5.329 10.059, 5.145 10.114, 4.933 10.158, 4.681 10.189, 4.375 10.2, 4.069 10.189, 3.817 10.158, 3.606 10.114, 3.421 10.059, 3.25 10, 3.079 9.941, 2.895 9.886, 2.683 9.842, 2.431 9.811, 2.125 9.8, 1.82 9.81, 1.574 9.831, 1.38 9.851, 1.23 9.856, 1.116 9.834, 1.029 9.773, 0.962 9.659, 0.907 9.48, 0.855 9.223, 0.8 8.875, 0.811 8.569, 0.842 8.317, 0.886 8.106, 0.941 7.921, 1 7.75, 1.059 7.579, 1.114 7.395, 1.158 7.183, 1.189 6.931, 1.2 6.625, 1.189 6.319, 1.158 6.067, 1.114 5.856, 1.059 5.671, 1 5.5, 0.941 5.329, 0.886 5.145, 0.842 4.933, 0.811 4.681, 0.8 4.375, 0.811 4.069, 0.842 3.817, 0.886 3.606, 0.941 3.421, 1 3.25, 1.059 3.079, 1.114 2.895, 1.158 2.683, 1.189 2.431, 1.2 2.125, 1.189 1.792, 1.158 1.486, 1.114 1.234, 1.059 1.063, 1 1)),((20 20, 20.03 20.1, 20.059 20.215, 20.087 20.34, 20.114 20.473, 20.138 20.61, 20.158 20.746, 20.176 20.879, 20.189 21.005, 20.197 21.119, 20.2 21.219, 20.189 21.551, 20.158 21.824, 20.114 22.053, 20.059 22.253, 20 22.439, 19.941 22.624, 19.886 22.824, 19.842 23.053, 19.811 23.326, 19.8 23.658, 19.811 23.99, 19.842 24.263, 19.886 24.492, 19.941 24.692, 20 24.877, 20.059 25.063, 20.114 25.263, 20.158 25.492, 20.189 25.765, 20.2 26.097, 20.189 26.428, 20.158 26.702, 20.114 26.931, 20.059 27.131, 20 27.316, 19.941 27.502, 19.886 27.701, 19.842 27.931, 19.811 28.204, 19.8 28.536, 19.812 28.865, 19.844 29.126, 19.896 29.321, 19.963 29.454, 20.043 29.529, 20.134 29.55, 20.233 29.521, 20.336 29.446, 20.442 29.327, 20.547 29.17, 20.79 28.943, 21.005 28.771, 21.198 28.641, 21.378 28.538, 21.551 28.449, 21.724 28.36, 21.904 28.257, 22.098 28.126, 22.312 27.955, 22.555 27.728, 22.781 27.486, 22.953 27.271, 23.083 27.077, 23.186 26.897, 23.276 26.724, 23.365 26.552, 23.468 26.372, 23.598 26.178, 23.77 25.963, 23.996 25.721, 24.239 25.494, 24.453 25.323, 24.647 25.192, 24.827 25.089, 25 25, 25.173 24.911, 25.353 24.808, 25.547 24.677, 25.761 24.506, 26.004 24.279, 26.23 24.037, 26.402 23.822, 26.532 23.628, 26.635 23.448, 26.724 23.276, 26.814 23.103, 26.917 22.923, 27.047 22.729, 27.219 22.514, 27.445 22.272, 27.688 22.045, 27.902 21.874, 28.096 21.743, 28.276 21.64, 28.449 21.551, 28.622 21.462, 28.802 21.359, 28.995 21.229, 29.21 21.057, 29.453 20.83, 29.533 20.562, 29.59 20.369, 29.618 20.24, 29.612 20.163, 29.565 20.129, 29.472 20.125, 29.328 20.141, 29.128 20.167, 28.866 20.19, 28.536 20.2, 28.204 20.189, 27.931 20.158, 27.701 20.114, 27.502 20.059, 27.316 20, 27.131 19.941, 26.931 19.886, 26.702 19.842, 26.428 19.811, 26.097 19.8, 25.765 19.811, 25.492 19.842, 25.263 19.886, 25.063 19.941, 24.877 20, 24.692 20.059, 24.492 20.114, 24.263 20.158, 23.99 20.189, 23.658 20.2, 23.326 20.189, 23.053 20.158, 22.824 20.114, 22.624 20.059, 22.439 20, 22.253 19.941, 22.053 19.886, 21.824 19.842, 21.551 19.811, 21.219 19.8, 21.005 19.811, 20.746 19.842, 20.473 19.886, 20.215 19.941, 20 20)))", + "MultiPolygon (((1 1, 1.092 0.97, 1.198 0.941, 1.314 0.913, 1.437 0.886, 1.563 0.863, 1.688 0.842, 1.811 0.824, 1.927 0.811, 2.033 0.803, 2.125 0.8, 2.431 0.811, 2.683 0.842, 2.894 0.886, 3.079 0.941, 3.25 1, 3.421 1.059, 3.606 1.114, 3.817 1.158, 4.069 1.189, 4.375 1.2, 4.681 1.189, 4.933 1.158, 5.144 1.114, 5.329 1.059, 5.5 1, 5.671 0.941, 5.856 0.886, 6.067 0.842, 6.319 0.811, 6.625 0.8, 6.931 0.811, 7.183 0.842, 7.394 0.886, 7.579 0.941, 7.75 1, 7.921 1.059, 8.105 1.114, 8.317 1.158, 8.569 1.189, 8.875 1.2, 9.18 1.19, 9.426 1.169, 9.62 1.149, 9.77 1.144, 9.884 1.166, 9.971 1.227, 10.038 1.341, 10.093 1.52, 10.145 1.777, 10.2 2.125, 10.189 2.431, 10.158 2.683, 10.114 2.894, 10.059 3.079, 10 3.25, 9.941 3.421, 9.886 3.606, 9.842 3.817, 9.811 4.069, 9.8 4.375, 9.811 4.681, 9.842 4.933, 9.886 5.144, 9.941 5.329, 10 5.5, 10.059 5.671, 10.114 5.856, 10.158 6.067, 10.189 6.319, 10.2 6.625, 10.189 6.931, 10.158 7.183, 10.114 7.394, 10.059 7.579, 10 7.75, 9.941 7.921, 9.886 8.105, 9.842 8.317, 9.811 8.569, 9.8 8.875, 9.81 9.18, 9.831 9.426, 9.851 9.62, 9.856 9.77, 9.834 9.884, 9.773 9.971, 9.659 10.038, 9.48 10.093, 9.223 10.145, 8.875 10.2, 8.569 10.189, 8.317 10.158, 8.105 10.114, 7.921 10.059, 7.75 10, 7.579 9.941, 7.394 9.886, 7.183 9.842, 6.931 9.811, 6.625 9.8, 6.319 9.811, 6.067 9.842, 5.856 9.886, 5.671 9.941, 5.5 10, 5.329 10.059, 5.144 10.114, 4.933 10.158, 4.681 10.189, 4.375 10.2, 4.069 10.189, 3.817 10.158, 3.605 10.114, 3.421 10.059, 3.25 10, 3.079 9.941, 2.894 9.886, 2.683 9.842, 2.431 9.811, 2.125 9.8, 1.82 9.81, 1.574 9.831, 1.38 9.851, 1.23 9.856, 1.116 9.834, 1.029 9.773, 0.962 9.659, 0.907 9.48, 0.855 9.223, 0.8 8.875, 0.811 8.569, 0.842 8.317, 0.886 8.105, 0.941 7.921, 1 7.75, 1.059 7.579, 1.114 7.394, 1.158 7.183, 1.189 6.931, 1.2 6.625, 1.189 6.319, 1.158 6.067, 1.114 5.856, 1.059 5.671, 1 5.5, 0.941 5.329, 0.886 5.144, 0.842 4.933, 0.811 4.681, 0.8 4.375, 0.811 4.069, 0.842 3.817, 0.886 3.605, 0.941 3.421, 1 3.25, 1.059 3.079, 1.114 2.894, 1.158 2.683, 1.189 2.431, 1.2 2.125, 1.189 1.792, 1.158 1.486, 1.114 1.234, 1.059 1.063, 1 1)),((20 20, 20.03 20.1, 20.059 20.215, 20.087 20.34, 20.114 20.473, 20.137 20.61, 20.158 20.746, 20.176 20.879, 20.189 21.005, 20.197 21.119, 20.2 21.219, 20.189 21.551, 20.158 21.824, 20.114 22.053, 20.059 22.253, 20 22.439, 19.941 22.624, 19.886 22.824, 19.842 23.053, 19.811 23.326, 19.8 23.658, 19.811 23.99, 19.842 24.263, 19.886 24.492, 19.941 24.692, 20 24.877, 20.059 25.063, 20.114 25.263, 20.158 25.492, 20.189 25.765, 20.2 26.097, 20.189 26.428, 20.158 26.702, 20.114 26.931, 20.059 27.131, 20 27.316, 19.941 27.502, 19.886 27.701, 19.842 27.931, 19.811 28.204, 19.8 28.536, 19.812 28.865, 19.844 29.126, 19.896 29.321, 19.963 29.454, 20.043 29.529, 20.134 29.55, 20.233 29.521, 20.336 29.446, 20.442 29.327, 20.547 29.17, 20.79 28.943, 21.005 28.771, 21.198 28.641, 21.378 28.538, 21.551 28.449, 21.724 28.36, 21.904 28.257, 22.098 28.126, 22.312 27.955, 22.555 27.728, 22.781 27.486, 22.953 27.271, 23.083 27.077, 23.186 26.897, 23.276 26.724, 23.365 26.552, 23.468 26.372, 23.598 26.178, 23.77 25.963, 23.996 25.721, 24.239 25.494, 24.453 25.323, 24.647 25.192, 24.827 25.089, 25 25, 25.173 24.911, 25.353 24.808, 25.547 24.677, 25.761 24.506, 26.004 24.279, 26.23 24.037, 26.402 23.822, 26.532 23.628, 26.635 23.448, 26.724 23.276, 26.814 23.103, 26.917 22.923, 27.047 22.729, 27.219 22.514, 27.445 22.272, 27.688 22.045, 27.902 21.874, 28.096 21.743, 28.276 21.64, 28.449 21.551, 28.622 21.462, 28.802 21.359, 28.995 21.229, 29.21 21.057, 29.453 20.83, 29.533 20.562, 29.59 20.369, 29.618 20.24, 29.612 20.163, 29.565 20.129, 29.472 20.125, 29.328 20.141, 29.128 20.167, 28.866 20.19, 28.536 20.2, 28.204 20.189, 27.931 20.158, 27.701 20.114, 27.502 20.059, 27.316 20, 27.131 19.941, 26.931 19.886, 26.702 19.842, 26.428 19.811, 26.097 19.8, 25.765 19.811, 25.492 19.842, 25.263 19.886, 25.063 19.941, 24.877 20, 24.692 20.059, 24.492 20.114, 24.263 20.158, 23.99 20.189, 23.658 20.2, 23.326 20.189, 23.053 20.158, 22.824 20.114, 22.624 20.059, 22.439 20, 22.253 19.941, 22.053 19.886, 21.824 19.842, 21.551 19.811, 21.219 19.8, 21.005 19.811, 20.746 19.842, 20.473 19.886, 20.215 19.941, 20 20)))", ) def testRoundRandomizedWaves(self): diff --git a/tests/src/python/test_qgsgeometrypaintdevice.py b/tests/src/python/test_qgsgeometrypaintdevice.py index 5acf813770e6..27e17fc1c686 100644 --- a/tests/src/python/test_qgsgeometrypaintdevice.py +++ b/tests/src/python/test_qgsgeometrypaintdevice.py @@ -559,7 +559,7 @@ def test_text(self): result.normalize() self.assertEqual( result.asWkt(2), - "GeometryCollection (Polygon ((57.33 -10.92, 57.33 -10.56, 57.34 -10.21, 57.36 -9.86, 57.39 -9.52, 57.42 -9.19, 57.46 -8.85, 57.51 -8.53, 57.57 -8.21, 57.63 -7.89, 57.7 -7.58, 57.78 -7.28, 57.86 -6.98, 57.96 -6.68, 58.06 -6.39, 58.16 -6.11, 58.28 -5.83, 58.4 -5.55, 58.53 -5.29, 58.67 -5.02, 58.81 -4.77, 58.97 -4.51, 59.13 -4.27, 59.29 -4.02, 59.47 -3.79, 59.65 -3.56, 59.84 -3.33, 60.04 -3.11, 60.24 -2.89, 60.45 -2.68, 60.67 -2.48, 60.9 -2.28, 61.13 -2.08, 61.37 -1.9, 61.61 -1.72, 61.86 -1.55, 62.11 -1.38, 62.37 -1.22, 62.64 -1.07, 62.91 -0.93, 63.19 -0.79, 63.47 -0.66, 63.76 -0.53, 64.05 -0.41, 64.35 -0.3, 64.66 -0.2, 64.97 -0.1, 65.28 -0.01, 65.61 0.08, 65.94 0.15, 66.27 0.22, 66.61 0.29, 66.95 0.35, 67.31 0.4, 67.66 0.44, 68.02 0.48, 68.39 0.51, 68.77 0.53, 69.15 0.55, 69.53 0.56, 69.92 0.56, 70.04 0.56, 70.15 0.56, 70.26 0.56, 70.38 0.56, 70.49 0.55, 70.6 0.55, 70.72 0.55, 70.83 0.54, 70.94 0.54, 71.06 0.53, 71.17 0.52, 71.28 0.51, 71.39 0.51, 71.51 0.5, 71.62 0.49, 71.73 0.48, 71.85 0.46, 71.96 0.45, 72.07 0.44, 72.19 0.43, 72.3 0.41, 72.41 0.4, 72.52 0.38, 72.64 0.37, 72.75 0.35, 72.86 0.33, 72.97 0.32, 73.09 0.3, 73.2 0.28, 73.31 0.26, 73.42 0.24, 73.54 0.22, 73.65 0.19, 73.76 0.17, 73.87 0.15, 73.99 0.12, 74.1 0.1, 74.21 0.07, 74.32 0.05, 74.43 0.02, 74.54 0, 74.66 -0.03, 74.77 -0.06, 74.88 -0.09, 74.99 -0.12, 75.1 -0.15, 75.21 -0.18, 75.32 -0.21, 75.43 -0.24, 75.54 -0.28, 75.65 -0.31, 75.76 -0.34, 75.87 -0.38, 75.98 -0.41, 76.09 -0.45, 76.2 -0.49, 76.31 -0.52, 76.42 -0.56, 76.53 -0.6, 76.64 -0.64, 76.64 -6.38, 76.56 -6.31, 76.47 -6.25, 76.39 -6.19, 76.3 -6.13, 76.22 -6.07, 76.13 -6.01, 76.05 -5.95, 75.96 -5.89, 75.87 -5.84, 75.78 -5.78, 75.69 -5.73, 75.61 -5.68, 75.52 -5.63, 75.43 -5.58, 75.34 -5.53, 75.25 -5.48, 75.15 -5.43, 75.06 -5.39, 74.97 -5.34, 74.88 -5.3, 74.78 -5.26, 74.69 -5.21, 74.6 -5.17, 74.5 -5.14, 74.41 -5.1, 74.31 -5.06, 74.21 -5.02, 74.12 -4.99, 74.02 -4.95, 73.92 -4.92, 73.82 -4.89, 73.73 -4.86, 73.63 -4.83, 73.53 -4.8, 73.43 -4.77, 73.33 -4.75, 73.23 -4.72, 73.13 -4.7, 73.03 -4.67, 72.92 -4.65, 72.82 -4.63, 72.72 -4.61, 72.62 -4.59, 72.51 -4.58, 72.41 -4.56, 72.31 -4.54, 72.2 -4.53, 72.1 -4.52, 71.99 -4.5, 71.89 -4.49, 71.78 -4.48, 71.68 -4.47, 71.57 -4.46, 71.46 -4.46, 71.35 -4.45, 71.25 -4.45, 71.14 -4.44, 71.03 -4.44, 70.92 -4.44, 70.81 -4.44, 70.62 -4.44, 70.43 -4.45, 70.24 -4.45, 70.05 -4.47, 69.87 -4.49, 69.69 -4.51, 69.51 -4.53, 69.34 -4.56, 69.17 -4.59, 69 -4.63, 68.83 -4.67, 68.67 -4.71, 68.51 -4.76, 68.35 -4.81, 68.2 -4.87, 68.05 -4.92, 67.9 -4.99, 67.76 -5.05, 67.61 -5.12, 67.48 -5.2, 67.34 -5.28, 67.21 -5.36, 67.08 -5.44, 66.95 -5.53, 66.82 -5.63, 66.7 -5.72, 66.58 -5.82, 66.47 -5.93, 66.35 -6.04, 66.24 -6.15, 66.14 -6.26, 66.03 -6.38, 65.93 -6.5, 65.84 -6.63, 65.74 -6.76, 65.65 -6.89, 65.57 -7.02, 65.49 -7.16, 65.41 -7.3, 65.34 -7.44, 65.26 -7.58, 65.2 -7.73, 65.13 -7.89, 65.07 -8.04, 65.02 -8.2, 64.96 -8.36, 64.92 -8.52, 64.87 -8.69, 64.83 -8.86, 64.79 -9.03, 64.76 -9.21, 64.73 -9.38, 64.7 -9.57, 64.67 -9.75, 64.65 -9.94, 64.64 -10.13, 64.63 -10.32, 64.62 -10.52, 64.61 -10.72, 64.61 -10.92, 64.61 -11.12, 64.62 -11.32, 64.63 -11.52, 64.64 -11.71, 64.65 -11.91, 64.67 -12.09, 64.7 -12.28, 64.73 -12.46, 64.76 -12.64, 64.79 -12.81, 64.83 -12.99, 64.87 -13.16, 64.92 -13.32, 64.96 -13.49, 65.02 -13.65, 65.07 -13.8, 65.13 -13.96, 65.2 -14.11, 65.26 -14.26, 65.34 -14.4, 65.41 -14.55, 65.49 -14.69, 65.57 -14.82, 65.65 -14.96, 65.74 -15.09, 65.84 -15.22, 65.93 -15.34, 66.03 -15.46, 66.14 -15.58, 66.24 -15.7, 66.35 -15.81, 66.47 -15.92, 66.58 -16.02, 66.7 -16.12, 66.82 -16.22, 66.95 -16.31, 67.08 -16.4, 67.21 -16.49, 67.34 -16.57, 67.48 -16.65, 67.61 -16.72, 67.76 -16.79, 67.9 -16.86, 68.05 -16.92, 68.2 -16.98, 68.35 -17.03, 68.51 -17.08, 68.67 -17.13, 68.83 -17.18, 69 -17.22, 69.17 -17.25, 69.34 -17.28, 69.51 -17.31, 69.69 -17.34, 69.87 -17.36, 70.05 -17.38, 70.24 -17.39, 70.43 -17.4, 70.62 -17.4, 70.81 -17.41, 70.91 -17.41, 71.02 -17.4, 71.12 -17.4, 71.22 -17.4, 71.32 -17.39, 71.42 -17.39, 71.52 -17.38, 71.62 -17.37, 71.72 -17.36, 71.82 -17.35, 71.92 -17.34, 72.02 -17.33, 72.12 -17.32, 72.22 -17.3, 72.32 -17.29, 72.42 -17.27, 72.52 -17.26, 72.62 -17.24, 72.71 -17.22, 72.81 -17.2, 72.91 -17.18, 73.01 -17.15, 73.11 -17.13, 73.2 -17.11, 73.3 -17.08, 73.4 -17.05, 73.49 -17.03, 73.59 -17, 73.69 -16.97, 73.78 -16.94, 73.88 -16.91, 73.97 -16.87, 74.07 -16.84, 74.16 -16.8, 74.26 -16.77, 74.36 -16.73, 74.45 -16.69, 74.55 -16.65, 74.64 -16.61, 74.74 -16.57, 74.83 -16.52, 74.93 -16.48, 75.02 -16.43, 75.12 -16.39, 75.21 -16.34, 75.31 -16.29, 75.41 -16.24, 75.5 -16.19, 75.6 -16.14, 75.69 -16.08, 75.79 -16.03, 75.88 -15.97, 75.98 -15.92, 76.07 -15.86, 76.17 -15.8, 76.26 -15.74, 76.36 -15.68, 76.45 -15.61, 76.55 -15.55, 76.64 -15.48, 76.64 -21.19, 76.53 -21.23, 76.42 -21.27, 76.31 -21.31, 76.2 -21.34, 76.09 -21.38, 75.98 -21.42, 75.87 -21.46, 75.76 -21.49, 75.65 -21.53, 75.53 -21.56, 75.42 -21.59, 75.31 -21.63, 75.2 -21.66, 75.09 -21.69, 74.98 -21.72, 74.87 -21.75, 74.76 -21.78, 74.65 -21.81, 74.54 -21.84, 74.43 -21.86, 74.31 -21.89, 74.2 -21.92, 74.09 -21.94, 73.98 -21.97, 73.87 -21.99, 73.76 -22.01, 73.65 -22.04, 73.54 -22.06, 73.42 -22.08, 73.31 -22.1, 73.2 -22.12, 73.09 -22.14, 72.98 -22.16, 72.87 -22.18, 72.75 -22.19, 72.64 -22.21, 72.53 -22.23, 72.42 -22.24, 72.31 -22.26, 72.19 -22.27, 72.08 -22.28, 71.97 -22.3, 71.85 -22.31, 71.74 -22.32, 71.63 -22.33, 71.52 -22.34, 71.4 -22.35, 71.29 -22.36, 71.18 -22.37, 71.06 -22.37, 70.95 -22.38, 70.84 -22.38, 70.72 -22.39, 70.61 -22.39, 70.49 -22.4, 70.38 -22.4, 70.27 -22.4, 70.15 -22.4, 70.04 -22.41, 69.92 -22.41, 69.53 -22.4, 69.15 -22.39, 68.77 -22.38, 68.39 -22.35, 68.02 -22.32, 67.66 -22.28, 67.31 -22.24, 66.95 -22.19, 66.61 -22.13, 66.27 -22.07, 65.94 -22, 65.61 -21.92, 65.28 -21.84, 64.97 -21.74, 64.66 -21.65, 64.35 -21.54, 64.05 -21.43, 63.76 -21.31, 63.47 -21.19, 63.19 -21.06, 62.91 -20.92, 62.64 -20.77, 62.37 -20.62, 62.11 -20.46, 61.86 -20.3, 61.61 -20.12, 61.37 -19.94, 61.13 -19.76, 60.9 -19.57, 60.67 -19.37, 60.45 -19.16, 60.24 -18.95, 60.04 -18.74, 59.84 -18.51, 59.65 -18.29, 59.47 -18.06, 59.29 -17.82, 59.13 -17.58, 58.97 -17.33, 58.81 -17.08, 58.67 -16.82, 58.53 -16.56, 58.4 -16.29, 58.28 -16.02, 58.16 -15.74, 58.06 -15.45, 57.96 -15.16, 57.86 -14.87, 57.78 -14.57, 57.7 -14.26, 57.63 -13.95, 57.57 -13.64, 57.51 -13.32, 57.46 -12.99, 57.42 -12.66, 57.39 -12.32, 57.36 -11.98, 57.34 -11.63, 57.33 -11.28, 57.33 -10.92)),Polygon ((1.72 -6.56, 1.72 -6.35, 1.73 -6.15, 1.74 -5.94, 1.76 -5.74, 1.78 -5.54, 1.8 -5.35, 1.83 -5.15, 1.87 -4.96, 1.91 -4.77, 1.95 -4.59, 2 -4.41, 2.06 -4.23, 2.12 -4.05, 2.18 -3.87, 2.25 -3.7, 2.32 -3.53, 2.4 -3.36, 2.48 -3.2, 2.57 -3.03, 2.66 -2.88, 2.76 -2.72, 2.86 -2.56, 2.96 -2.41, 3.07 -2.26, 3.19 -2.12, 3.31 -1.97, 3.43 -1.83, 3.56 -1.69, 3.7 -1.56, 3.84 -1.42, 3.98 -1.29, 4.12 -1.17, 4.27 -1.04, 4.42 -0.93, 4.58 -0.82, 4.73 -0.71, 4.89 -0.6, 5.06 -0.5, 5.22 -0.41, 5.39 -0.32, 5.56 -0.23, 5.74 -0.15, 5.91 -0.07, 6.09 0, 6.28 0.07, 6.46 0.13, 6.65 0.19, 6.84 0.25, 7.03 0.3, 7.23 0.34, 7.43 0.38, 7.63 0.42, 7.84 0.45, 8.05 0.48, 8.26 0.51, 8.47 0.53, 8.69 0.54, 8.91 0.55, 9.13 0.56, 9.36 0.56, 9.53 0.56, 9.69 0.56, 9.85 0.55, 10.02 0.55, 10.18 0.54, 10.34 0.53, 10.49 0.51, 10.65 0.5, 10.8 0.48, 10.95 0.46, 11.1 0.44, 11.25 0.42, 11.4 0.39, 11.54 0.37, 11.69 0.34, 11.83 0.3, 11.97 0.27, 12.11 0.24, 12.24 0.2, 12.38 0.16, 12.51 0.12, 12.64 0.08, 12.77 0.03, 12.9 -0.02, 13.03 -0.07, 13.15 -0.12, 13.27 -0.17, 13.4 -0.23, 13.51 -0.28, 13.63 -0.34, 13.75 -0.41, 13.87 -0.47, 13.98 -0.54, 14.1 -0.6, 14.21 -0.68, 14.32 -0.75, 14.43 -0.83, 14.55 -0.9, 14.66 -0.99, 14.77 -1.07, 14.87 -1.16, 14.98 -1.24, 15.09 -1.33, 15.2 -1.43, 15.3 -1.52, 15.41 -1.62, 15.51 -1.72, 15.62 -1.83, 15.72 -1.93, 15.82 -2.04, 15.92 -2.15, 16.02 -2.26, 16.12 -2.38, 16.22 -2.49, 16.32 -2.61, 16.42 -2.74, 16.51 -2.86, 16.61 -2.99, 16.7 -3.12, 16.8 -3.25, 16.8 0, 23.84 0, 23.84 -12.48, 23.84 -12.83, 23.83 -13.17, 23.82 -13.51, 23.8 -13.83, 23.77 -14.15, 23.74 -14.47, 23.7 -14.78, 23.66 -15.08, 23.61 -15.37, 23.55 -15.66, 23.49 -15.94, 23.42 -16.22, 23.35 -16.49, 23.27 -16.75, 23.19 -17.01, 23.1 -17.26, 23 -17.5, 22.9 -17.74, 22.79 -17.97, 22.68 -18.19, 22.56 -18.41, 22.43 -18.62, 22.3 -18.82, 22.16 -19.02, 22.02 -19.21, 21.87 -19.4, 21.72 -19.57, 21.56 -19.75, 21.39 -19.91, 21.22 -20.07, 21.04 -20.22, 20.85 -20.37, 20.66 -20.51, 20.46 -20.65, 20.25 -20.78, 20.04 -20.91, 19.81 -21.03, 19.58 -21.15, 19.35 -21.26, 19.1 -21.37, 18.85 -21.47, 18.59 -21.57, 18.32 -21.66, 18.05 -21.74, 17.77 -21.82, 17.48 -21.9, 17.19 -21.97, 16.88 -22.03, 16.57 -22.09, 16.25 -22.15, 15.93 -22.2, 15.6 -22.24, 15.26 -22.28, 14.91 -22.31, 14.55 -22.34, 14.19 -22.36, 13.82 -22.38, 13.45 -22.4, 13.06 -22.4, 12.67 -22.41, 12.52 -22.41, 12.37 -22.41, 12.22 -22.4, 12.07 -22.4, 11.92 -22.4, 11.77 -22.4, 11.61 -22.39, 11.46 -22.39, 11.31 -22.38, 11.16 -22.38, 11.01 -22.37, 10.86 -22.36, 10.71 -22.35, 10.56 -22.35, 10.41 -22.34, 10.26 -22.33, 10.1 -22.32, 9.95 -22.31, 9.8 -22.29, 9.65 -22.28, 9.5 -22.27, 9.35 -22.26, 9.2 -22.24, 9.05 -22.23, 8.9 -22.21, 8.74 -22.2, 8.59 -22.18, 8.44 -22.16, 8.29 -22.14, 8.14 -22.13, 7.99 -22.11, 7.84 -22.09, 7.69 -22.07, 7.54 -22.05, 7.39 -22.02, 7.24 -22, 7.09 -21.98, 6.93 -21.96, 6.78 -21.93, 6.63 -21.91, 6.48 -21.88, 6.33 -21.86, 6.18 -21.83, 6.03 -21.8, 5.88 -21.78, 5.73 -21.75, 5.58 -21.72, 5.43 -21.69, 5.28 -21.66, 5.13 -21.63, 4.98 -21.6, 4.83 -21.57, 4.69 -21.54, 4.54 -21.51, 4.39 -21.47, 4.24 -21.44, 4.09 -21.4, 3.94 -21.37, 3.79 -21.33, 3.64 -21.3, 3.64 -15.95, 3.75 -16.01, 3.86 -16.07, 3.97 -16.13, 4.09 -16.19, 4.2 -16.24, 4.31 -16.3, 4.43 -16.35, 4.54 -16.4, 4.66 -16.46, 4.78 -16.51, 4.89 -16.56, 5.01 -16.6, 5.13 -16.65, 5.25 -16.7, 5.37 -16.74, 5.49 -16.79, 5.61 -16.83, 5.73 -16.87, 5.85 -16.92, 5.97 -16.96, 6.09 -17, 6.22 -17.03, 6.34 -17.07, 6.47 -17.11, 6.59 -17.14, 6.72 -17.18, 6.84 -17.21, 6.97 -17.24, 7.1 -17.27, 7.23 -17.3, 7.36 -17.33, 7.49 -17.36, 7.62 -17.39, 7.75 -17.42, 7.88 -17.44, 8.01 -17.47, 8.14 -17.49, 8.28 -17.51, 8.41 -17.53, 8.55 -17.55, 8.68 -17.57, 8.82 -17.59, 8.96 -17.61, 9.1 -17.62, 9.24 -17.64, 9.38 -17.65, 9.52 -17.67, 9.66 -17.68, 9.8 -17.69, 9.94 -17.7, 10.09 -17.71, 10.23 -17.72, 10.37 -17.73, 10.52 -17.73, 10.67 -17.74, 10.81 -17.74, 10.96 -17.75, 11.11 -17.75, 11.26 -17.75, 11.41 -17.75, 11.59 -17.75, 11.77 -17.75, 11.95 -17.74, 12.12 -17.74, 12.29 -17.73, 12.46 -17.72, 12.62 -17.71, 12.78 -17.7, 12.94 -17.68, 13.1 -17.66, 13.25 -17.65, 13.4 -17.63, 13.54 -17.61, 13.68 -17.58, 13.82 -17.56, 13.95 -17.53, 14.08 -17.5, 14.21 -17.47, 14.34 -17.44, 14.46 -17.41, 14.58 -17.37, 14.69 -17.34, 14.8 -17.3, 14.91 -17.26, 15.02 -17.22, 15.12 -17.17, 15.22 -17.13, 15.31 -17.08, 15.4 -17.03, 15.49 -16.98, 15.58 -16.93, 15.66 -16.88, 15.74 -16.82, 15.82 -16.76, 15.89 -16.7, 15.96 -16.64, 16.03 -16.58, 16.1 -16.51, 16.16 -16.44, 16.22 -16.37, 16.27 -16.3, 16.33 -16.23, 16.38 -16.15, 16.43 -16.07, 16.47 -15.99, 16.51 -15.91, 16.55 -15.83, 16.59 -15.74, 16.62 -15.65, 16.65 -15.56, 16.68 -15.47, 16.7 -15.37, 16.73 -15.28, 16.74 -15.18, 16.76 -15.08, 16.77 -14.98, 16.78 -14.87, 16.79 -14.77, 16.8 -14.66, 16.8 -14.55, 16.8 -14, 12.67 -14, 12.3 -14, 11.93 -13.99, 11.57 -13.98, 11.22 -13.97, 10.87 -13.95, 10.53 -13.93, 10.2 -13.9, 9.87 -13.87, 9.55 -13.84, 9.24 -13.8, 8.93 -13.76, 8.63 -13.71, 8.33 -13.66, 8.05 -13.61, 7.77 -13.55, 7.49 -13.49, 7.23 -13.42, 6.97 -13.35, 6.71 -13.28, 6.47 -13.2, 6.23 -13.12, 5.99 -13.03, 5.77 -12.94, 5.55 -12.85, 5.33 -12.75, 5.13 -12.65, 4.93 -12.54, 4.73 -12.43, 4.55 -12.32, 4.37 -12.2, 4.19 -12.08, 4.03 -11.95, 3.86 -11.82, 3.71 -11.69, 3.56 -11.55, 3.41 -11.4, 3.28 -11.25, 3.14 -11.1, 3.02 -10.94, 2.9 -10.78, 2.78 -10.61, 2.67 -10.44, 2.57 -10.26, 2.47 -10.08, 2.38 -9.89, 2.3 -9.7, 2.22 -9.51, 2.14 -9.31, 2.07 -9.11, 2.01 -8.9, 1.96 -8.68, 1.91 -8.47, 1.86 -8.24, 1.82 -8.02, 1.79 -7.79, 1.77 -7.55, 1.75 -7.31, 1.73 -7.07, 1.72 -6.82, 1.72 -6.56),(8.77 -6.92, 8.77 -7.02, 8.77 -7.11, 8.78 -7.2, 8.79 -7.29, 8.8 -7.38, 8.81 -7.47, 8.83 -7.55, 8.84 -7.64, 8.87 -7.72, 8.89 -7.8, 8.91 -7.88, 8.94 -7.96, 8.97 -8.04, 9.01 -8.11, 9.04 -8.19, 9.08 -8.26, 9.12 -8.33, 9.17 -8.4, 9.21 -8.46, 9.26 -8.53, 9.31 -8.59, 9.36 -8.66, 9.42 -8.72, 9.48 -8.78, 9.54 -8.83, 9.6 -8.89, 9.66 -8.95, 9.73 -9, 9.8 -9.05, 9.88 -9.1, 9.95 -9.15, 10.03 -9.2, 10.11 -9.24, 10.19 -9.29, 10.27 -9.33, 10.36 -9.37, 10.45 -9.41, 10.54 -9.44, 10.64 -9.48, 10.73 -9.51, 10.83 -9.55, 10.94 -9.58, 11.04 -9.61, 11.15 -9.63, 11.25 -9.66, 11.37 -9.68, 11.48 -9.7, 11.59 -9.73, 11.71 -9.74, 11.83 -9.76, 11.96 -9.78, 12.08 -9.79, 12.21 -9.8, 12.34 -9.81, 12.47 -9.82, 12.61 -9.83, 12.75 -9.84, 12.88 -9.84, 13.03 -9.84, 13.17 -9.84, 16.8 -9.84, 16.8 -9.05, 16.8 -8.91, 16.79 -8.77, 16.78 -8.63, 16.77 -8.49, 16.76 -8.36, 16.74 -8.23, 16.72 -8.09, 16.7 -7.96, 16.68 -7.84, 16.65 -7.71, 16.62 -7.58, 16.58 -7.46, 16.54 -7.34, 16.5 -7.22, 16.46 -7.1, 16.41 -6.98, 16.37 -6.86, 16.31 -6.75, 16.26 -6.64, 16.2 -6.53, 16.14 -6.42, 16.07 -6.31, 16.01 -6.2, 15.94 -6.1, 15.86 -5.99, 15.79 -5.89, 15.71 -5.79, 15.63 -5.69, 15.54 -5.6, 15.45 -5.5, 15.36 -5.41, 15.27 -5.32, 15.18 -5.23, 15.08 -5.15, 14.99 -5.07, 14.89 -4.99, 14.79 -4.91, 14.69 -4.84, 14.59 -4.77, 14.49 -4.71, 14.38 -4.65, 14.28 -4.59, 14.17 -4.53, 14.06 -4.48, 13.95 -4.43, 13.84 -4.39, 13.72 -4.35, 13.61 -4.31, 13.49 -4.27, 13.38 -4.24, 13.26 -4.21, 13.14 -4.18, 13.02 -4.16, 12.89 -4.14, 12.77 -4.12, 12.64 -4.1, 12.52 -4.09, 12.39 -4.08, 12.26 -4.08, 12.13 -4.08, 12.02 -4.08, 11.92 -4.08, 11.82 -4.09, 11.72 -4.09, 11.62 -4.1, 11.52 -4.11, 11.43 -4.12, 11.33 -4.13, 11.24 -4.15, 11.15 -4.16, 11.06 -4.18, 10.97 -4.2, 10.89 -4.22, 10.8 -4.24, 10.72 -4.27, 10.64 -4.29, 10.56 -4.32, 10.48 -4.35, 10.4 -4.38, 10.33 -4.41, 10.25 -4.45, 10.18 -4.49, 10.11 -4.52, 10.04 -4.56, 9.97 -4.6, 9.91 -4.65, 9.84 -4.69, 9.78 -4.74, 9.72 -4.79, 9.66 -4.84, 9.6 -4.89, 9.54 -4.94, 9.49 -4.99, 9.43 -5.05, 9.38 -5.1, 9.34 -5.16, 9.29 -5.22, 9.24 -5.28, 9.2 -5.34, 9.16 -5.4, 9.12 -5.47, 9.09 -5.53, 9.05 -5.6, 9.02 -5.67, 8.99 -5.74, 8.96 -5.81, 8.93 -5.88, 8.91 -5.95, 8.89 -6.02, 8.86 -6.1, 8.85 -6.18, 8.83 -6.25, 8.81 -6.33, 8.8 -6.41, 8.79 -6.5, 8.78 -6.58, 8.77 -6.66, 8.77 -6.75, 8.77 -6.83, 8.77 -6.92)),Polygon ((30.34 -30.39, 30.34 0, 37.34 0, 37.34 -3.17, 37.44 -3.04, 37.54 -2.92, 37.63 -2.8, 37.73 -2.68, 37.83 -2.56, 37.93 -2.44, 38.03 -2.33, 38.13 -2.22, 38.24 -2.11, 38.34 -2, 38.44 -1.9, 38.55 -1.8, 38.65 -1.7, 38.76 -1.6, 38.86 -1.5, 38.97 -1.41, 39.08 -1.32, 39.18 -1.23, 39.29 -1.14, 39.4 -1.06, 39.51 -0.98, 39.62 -0.9, 39.74 -0.82, 39.85 -0.75, 39.96 -0.67, 40.08 -0.6, 40.19 -0.54, 40.31 -0.47, 40.42 -0.41, 40.54 -0.34, 40.66 -0.28, 40.78 -0.23, 40.9 -0.17, 41.02 -0.12, 41.14 -0.07, 41.26 -0.02, 41.39 0.03, 41.51 0.08, 41.64 0.12, 41.77 0.16, 41.9 0.2, 42.03 0.24, 42.16 0.27, 42.29 0.3, 42.43 0.34, 42.56 0.37, 42.7 0.39, 42.84 0.42, 42.98 0.44, 43.12 0.46, 43.26 0.48, 43.4 0.5, 43.54 0.51, 43.69 0.53, 43.83 0.54, 43.98 0.55, 44.13 0.55, 44.28 0.56, 44.43 0.56, 44.58 0.56, 44.85 0.56, 45.11 0.55, 45.37 0.53, 45.63 0.51, 45.89 0.47, 46.14 0.43, 46.39 0.39, 46.63 0.33, 46.88 0.27, 47.12 0.21, 47.35 0.13, 47.58 0.05, 47.81 -0.04, 48.04 -0.14, 48.26 -0.24, 48.48 -0.35, 48.7 -0.47, 48.91 -0.59, 49.12 -0.73, 49.33 -0.86, 49.54 -1.01, 49.74 -1.16, 49.93 -1.32, 50.13 -1.49, 50.32 -1.67, 50.51 -1.85, 50.69 -2.04, 50.87 -2.23, 51.05 -2.44, 51.23 -2.65, 51.4 -2.86, 51.56 -3.08, 51.72 -3.31, 51.87 -3.54, 52.02 -3.77, 52.16 -4.01, 52.3 -4.25, 52.43 -4.49, 52.55 -4.74, 52.67 -4.99, 52.78 -5.25, 52.89 -5.51, 52.99 -5.78, 53.09 -6.05, 53.18 -6.32, 53.26 -6.6, 53.34 -6.88, 53.41 -7.17, 53.48 -7.46, 53.54 -7.75, 53.59 -8.05, 53.64 -8.35, 53.69 -8.66, 53.72 -8.97, 53.76 -9.29, 53.78 -9.6, 53.8 -9.93, 53.82 -10.26, 53.83 -10.59, 53.83 -10.92, 53.83 -11.26, 53.82 -11.59, 53.8 -11.92, 53.78 -12.24, 53.76 -12.56, 53.72 -12.87, 53.69 -13.18, 53.64 -13.49, 53.59 -13.79, 53.54 -14.09, 53.48 -14.39, 53.41 -14.68, 53.34 -14.96, 53.26 -15.24, 53.18 -15.52, 53.09 -15.8, 52.99 -16.06, 52.89 -16.33, 52.78 -16.59, 52.67 -16.85, 52.55 -17.1, 52.43 -17.35, 52.3 -17.6, 52.16 -17.84, 52.02 -18.07, 51.87 -18.31, 51.72 -18.53, 51.56 -18.76, 51.4 -18.98, 51.23 -19.2, 51.05 -19.41, 50.87 -19.61, 50.69 -19.81, 50.51 -19.99, 50.32 -20.18, 50.13 -20.35, 49.93 -20.52, 49.74 -20.68, 49.54 -20.83, 49.33 -20.98, 49.12 -21.12, 48.91 -21.25, 48.7 -21.38, 48.48 -21.49, 48.26 -21.6, 48.04 -21.71, 47.81 -21.8, 47.58 -21.89, 47.35 -21.97, 47.12 -22.05, 46.88 -22.12, 46.63 -22.18, 46.39 -22.23, 46.14 -22.28, 45.89 -22.32, 45.63 -22.35, 45.37 -22.37, 45.11 -22.39, 44.85 -22.4, 44.58 -22.41, 44.43 -22.41, 44.28 -22.4, 44.13 -22.4, 43.98 -22.39, 43.83 -22.38, 43.69 -22.37, 43.54 -22.36, 43.4 -22.34, 43.26 -22.32, 43.12 -22.31, 42.98 -22.28, 42.84 -22.26, 42.7 -22.24, 42.56 -22.21, 42.43 -22.18, 42.29 -22.15, 42.16 -22.12, 42.03 -22.08, 41.9 -22.04, 41.77 -22, 41.64 -21.96, 41.51 -21.92, 41.39 -21.87, 41.26 -21.83, 41.14 -21.78, 41.02 -21.73, 40.9 -21.67, 40.78 -21.62, 40.66 -21.56, 40.54 -21.5, 40.42 -21.44, 40.31 -21.37, 40.19 -21.31, 40.08 -21.24, 39.96 -21.17, 39.85 -21.1, 39.74 -21.02, 39.62 -20.94, 39.51 -20.86, 39.4 -20.78, 39.29 -20.7, 39.18 -20.61, 39.08 -20.52, 38.97 -20.43, 38.86 -20.34, 38.76 -20.24, 38.65 -20.15, 38.55 -20.05, 38.44 -19.94, 38.34 -19.84, 38.24 -19.73, 38.13 -19.62, 38.03 -19.51, 37.93 -19.4, 37.83 -19.28, 37.73 -19.17, 37.63 -19.05, 37.54 -18.92, 37.44 -18.8, 37.34 -18.67, 37.34 -30.39, 30.34 -30.39),(37.34 -10.92, 37.35 -11.13, 37.35 -11.33, 37.36 -11.53, 37.37 -11.73, 37.38 -11.92, 37.39 -12.11, 37.41 -12.29, 37.43 -12.48, 37.45 -12.66, 37.48 -12.83, 37.5 -13.01, 37.54 -13.18, 37.57 -13.34, 37.6 -13.51, 37.64 -13.67, 37.68 -13.82, 37.73 -13.98, 37.77 -14.13, 37.82 -14.27, 37.88 -14.42, 37.93 -14.56, 37.99 -14.7, 38.05 -14.83, 38.11 -14.96, 38.17 -15.09, 38.24 -15.21, 38.31 -15.34, 38.38 -15.45, 38.46 -15.57, 38.54 -15.68, 38.62 -15.79, 38.7 -15.89, 38.79 -15.99, 38.88 -16.09, 38.97 -16.18, 39.06 -16.27, 39.15 -16.36, 39.25 -16.44, 39.35 -16.52, 39.45 -16.6, 39.56 -16.67, 39.66 -16.73, 39.77 -16.8, 39.88 -16.86, 40 -16.92, 40.11 -16.97, 40.23 -17.02, 40.35 -17.06, 40.48 -17.11, 40.6 -17.14, 40.73 -17.18, 40.86 -17.21, 40.99 -17.24, 41.13 -17.26, 41.26 -17.28, 41.4 -17.3, 41.54 -17.31, 41.69 -17.32, 41.84 -17.33, 41.98 -17.33, 42.13 -17.33, 42.28 -17.32, 42.42 -17.31, 42.57 -17.3, 42.7 -17.28, 42.84 -17.26, 42.98 -17.24, 43.11 -17.21, 43.24 -17.18, 43.37 -17.15, 43.49 -17.11, 43.61 -17.07, 43.73 -17.02, 43.85 -16.97, 43.97 -16.92, 44.08 -16.86, 44.19 -16.8, 44.3 -16.74, 44.41 -16.67, 44.51 -16.6, 44.61 -16.52, 44.71 -16.45, 44.81 -16.36, 44.9 -16.28, 44.99 -16.19, 45.08 -16.1, 45.17 -16, 45.25 -15.9, 45.33 -15.8, 45.41 -15.69, 45.49 -15.58, 45.57 -15.46, 45.64 -15.34, 45.71 -15.22, 45.77 -15.1, 45.84 -14.97, 45.9 -14.84, 45.96 -14.71, 46.02 -14.57, 46.07 -14.43, 46.12 -14.28, 46.17 -14.14, 46.21 -13.99, 46.26 -13.83, 46.3 -13.68, 46.34 -13.52, 46.37 -13.35, 46.41 -13.18, 46.44 -13.01, 46.46 -12.84, 46.49 -12.66, 46.51 -12.48, 46.53 -12.3, 46.55 -12.11, 46.56 -11.92, 46.57 -11.73, 46.58 -11.53, 46.59 -11.33, 46.59 -11.13, 46.59 -10.92, 46.59 -10.72, 46.59 -10.51, 46.58 -10.31, 46.57 -10.11, 46.56 -9.92, 46.55 -9.73, 46.53 -9.54, 46.51 -9.36, 46.49 -9.18, 46.46 -9, 46.44 -8.83, 46.41 -8.66, 46.37 -8.49, 46.34 -8.33, 46.3 -8.17, 46.26 -8.01, 46.21 -7.86, 46.17 -7.71, 46.12 -7.56, 46.07 -7.41, 46.02 -7.27, 45.96 -7.14, 45.9 -7, 45.84 -6.87, 45.77 -6.74, 45.71 -6.62, 45.64 -6.5, 45.57 -6.38, 45.49 -6.27, 45.41 -6.16, 45.33 -6.05, 45.25 -5.94, 45.17 -5.84, 45.08 -5.75, 44.99 -5.65, 44.9 -5.57, 44.81 -5.48, 44.71 -5.4, 44.61 -5.32, 44.51 -5.24, 44.41 -5.17, 44.3 -5.11, 44.19 -5.04, 44.08 -4.98, 43.97 -4.93, 43.85 -4.87, 43.73 -4.82, 43.61 -4.78, 43.49 -4.74, 43.37 -4.7, 43.24 -4.66, 43.11 -4.63, 42.98 -4.6, 42.84 -4.58, 42.7 -4.56, 42.57 -4.54, 42.42 -4.53, 42.28 -4.52, 42.13 -4.52, 41.98 -4.52, 41.84 -4.52, 41.69 -4.52, 41.54 -4.53, 41.4 -4.54, 41.26 -4.56, 41.13 -4.58, 40.99 -4.61, 40.86 -4.63, 40.73 -4.66, 40.6 -4.7, 40.48 -4.74, 40.35 -4.78, 40.23 -4.83, 40.11 -4.87, 40 -4.93, 39.88 -4.98, 39.77 -5.04, 39.66 -5.11, 39.56 -5.18, 39.45 -5.25, 39.35 -5.32, 39.25 -5.4, 39.15 -5.48, 39.06 -5.57, 38.97 -5.66, 38.88 -5.75, 38.79 -5.85, 38.7 -5.95, 38.62 -6.06, 38.54 -6.16, 38.46 -6.28, 38.39 -6.39, 38.31 -6.51, 38.24 -6.63, 38.17 -6.75, 38.11 -6.88, 38.05 -7.01, 37.99 -7.15, 37.93 -7.28, 37.88 -7.43, 37.82 -7.57, 37.77 -7.72, 37.73 -7.87, 37.68 -8.02, 37.64 -8.18, 37.6 -8.34, 37.57 -8.5, 37.54 -8.67, 37.5 -8.84, 37.48 -9.01, 37.45 -9.19, 37.43 -9.37, 37.41 -9.55, 37.39 -9.74, 37.38 -9.93, 37.37 -10.12, 37.36 -10.31, 37.35 -10.51, 37.35 -10.72, 37.34 -10.92)))", + "GeometryCollection (Polygon ((57.33 -10.92, 57.33 -10.56, 57.34 -10.21, 57.36 -9.86, 57.39 -9.52, 57.42 -9.19, 57.46 -8.85, 57.51 -8.53, 57.57 -8.21, 57.63 -7.89, 57.7 -7.58, 57.78 -7.28, 57.86 -6.98, 57.96 -6.68, 58.06 -6.39, 58.16 -6.11, 58.28 -5.83, 58.4 -5.55, 58.53 -5.29, 58.67 -5.02, 58.81 -4.77, 58.97 -4.51, 59.13 -4.27, 59.29 -4.02, 59.47 -3.79, 59.65 -3.56, 59.84 -3.33, 60.04 -3.11, 60.24 -2.89, 60.45 -2.68, 60.67 -2.48, 60.9 -2.28, 61.13 -2.08, 61.37 -1.9, 61.61 -1.72, 61.86 -1.55, 62.11 -1.38, 62.37 -1.22, 62.64 -1.07, 62.91 -0.93, 63.19 -0.79, 63.47 -0.66, 63.76 -0.53, 64.05 -0.41, 64.35 -0.3, 64.66 -0.2, 64.97 -0.1, 65.28 -0.01, 65.61 0.08, 65.94 0.15, 66.27 0.22, 66.61 0.29, 66.95 0.35, 67.31 0.4, 67.66 0.44, 68.02 0.48, 68.39 0.51, 68.77 0.53, 69.15 0.55, 69.53 0.56, 69.92 0.56, 70.04 0.56, 70.15 0.56, 70.26 0.56, 70.38 0.56, 70.49 0.55, 70.6 0.55, 70.72 0.55, 70.83 0.54, 70.94 0.54, 71.06 0.53, 71.17 0.52, 71.28 0.51, 71.39 0.51, 71.51 0.5, 71.62 0.49, 71.73 0.48, 71.85 0.46, 71.96 0.45, 72.07 0.44, 72.19 0.43, 72.3 0.41, 72.41 0.4, 72.52 0.38, 72.64 0.37, 72.75 0.35, 72.86 0.33, 72.97 0.32, 73.09 0.3, 73.2 0.28, 73.31 0.26, 73.42 0.24, 73.54 0.22, 73.65 0.19, 73.76 0.17, 73.87 0.15, 73.99 0.12, 74.1 0.1, 74.21 0.07, 74.32 0.05, 74.43 0.02, 74.54 0, 74.66 -0.03, 74.77 -0.06, 74.88 -0.09, 74.99 -0.12, 75.1 -0.15, 75.21 -0.18, 75.32 -0.21, 75.43 -0.24, 75.54 -0.28, 75.65 -0.31, 75.76 -0.34, 75.87 -0.38, 75.98 -0.41, 76.09 -0.45, 76.2 -0.49, 76.31 -0.52, 76.42 -0.56, 76.53 -0.6, 76.64 -0.64, 76.64 -6.38, 76.56 -6.31, 76.47 -6.25, 76.39 -6.19, 76.3 -6.13, 76.22 -6.07, 76.13 -6.01, 76.05 -5.95, 75.96 -5.89, 75.87 -5.84, 75.78 -5.78, 75.69 -5.73, 75.61 -5.68, 75.52 -5.63, 75.43 -5.58, 75.34 -5.53, 75.25 -5.48, 75.15 -5.43, 75.06 -5.39, 74.97 -5.34, 74.88 -5.3, 74.78 -5.26, 74.69 -5.21, 74.6 -5.17, 74.5 -5.14, 74.41 -5.1, 74.31 -5.06, 74.21 -5.02, 74.12 -4.99, 74.02 -4.95, 73.92 -4.92, 73.82 -4.89, 73.73 -4.86, 73.63 -4.83, 73.53 -4.8, 73.43 -4.77, 73.33 -4.75, 73.23 -4.72, 73.13 -4.7, 73.03 -4.67, 72.92 -4.65, 72.82 -4.63, 72.72 -4.61, 72.62 -4.59, 72.51 -4.58, 72.41 -4.56, 72.31 -4.54, 72.2 -4.53, 72.1 -4.52, 71.99 -4.5, 71.89 -4.49, 71.78 -4.48, 71.68 -4.47, 71.57 -4.46, 71.46 -4.46, 71.35 -4.45, 71.25 -4.45, 71.14 -4.44, 71.03 -4.44, 70.92 -4.44, 70.81 -4.44, 70.62 -4.44, 70.43 -4.45, 70.24 -4.45, 70.05 -4.47, 69.87 -4.49, 69.69 -4.51, 69.51 -4.53, 69.34 -4.56, 69.17 -4.59, 69 -4.63, 68.83 -4.67, 68.67 -4.71, 68.51 -4.76, 68.35 -4.81, 68.2 -4.87, 68.05 -4.92, 67.9 -4.99, 67.76 -5.05, 67.61 -5.12, 67.48 -5.2, 67.34 -5.28, 67.21 -5.36, 67.08 -5.44, 66.95 -5.53, 66.82 -5.63, 66.7 -5.72, 66.58 -5.82, 66.47 -5.93, 66.35 -6.04, 66.24 -6.15, 66.14 -6.26, 66.03 -6.38, 65.93 -6.5, 65.84 -6.63, 65.74 -6.76, 65.65 -6.89, 65.57 -7.02, 65.49 -7.16, 65.41 -7.3, 65.34 -7.44, 65.26 -7.58, 65.2 -7.73, 65.13 -7.89, 65.07 -8.04, 65.02 -8.2, 64.96 -8.36, 64.92 -8.52, 64.87 -8.69, 64.83 -8.86, 64.79 -9.03, 64.76 -9.21, 64.73 -9.38, 64.7 -9.57, 64.67 -9.75, 64.65 -9.94, 64.64 -10.13, 64.63 -10.32, 64.62 -10.52, 64.61 -10.72, 64.61 -10.92, 64.61 -11.12, 64.62 -11.32, 64.63 -11.52, 64.64 -11.71, 64.65 -11.91, 64.67 -12.09, 64.7 -12.28, 64.73 -12.46, 64.76 -12.64, 64.79 -12.81, 64.83 -12.99, 64.87 -13.16, 64.92 -13.32, 64.96 -13.49, 65.02 -13.65, 65.07 -13.8, 65.13 -13.96, 65.2 -14.11, 65.26 -14.26, 65.34 -14.4, 65.41 -14.55, 65.49 -14.69, 65.57 -14.82, 65.65 -14.96, 65.74 -15.09, 65.84 -15.22, 65.93 -15.34, 66.03 -15.46, 66.14 -15.58, 66.24 -15.7, 66.35 -15.81, 66.47 -15.92, 66.58 -16.02, 66.7 -16.12, 66.82 -16.22, 66.95 -16.31, 67.08 -16.4, 67.21 -16.49, 67.34 -16.57, 67.48 -16.65, 67.61 -16.72, 67.76 -16.79, 67.9 -16.86, 68.05 -16.92, 68.2 -16.98, 68.35 -17.03, 68.51 -17.08, 68.67 -17.13, 68.83 -17.18, 69 -17.22, 69.17 -17.25, 69.34 -17.28, 69.51 -17.31, 69.69 -17.34, 69.87 -17.36, 70.05 -17.38, 70.24 -17.39, 70.43 -17.4, 70.62 -17.4, 70.81 -17.41, 70.91 -17.41, 71.02 -17.4, 71.12 -17.4, 71.22 -17.4, 71.32 -17.39, 71.42 -17.39, 71.52 -17.38, 71.62 -17.37, 71.72 -17.36, 71.82 -17.35, 71.92 -17.34, 72.02 -17.33, 72.12 -17.32, 72.22 -17.3, 72.32 -17.29, 72.42 -17.27, 72.52 -17.26, 72.62 -17.24, 72.71 -17.22, 72.81 -17.2, 72.91 -17.18, 73.01 -17.15, 73.11 -17.13, 73.2 -17.11, 73.3 -17.08, 73.4 -17.05, 73.49 -17.03, 73.59 -17, 73.69 -16.97, 73.78 -16.94, 73.88 -16.91, 73.97 -16.87, 74.07 -16.84, 74.16 -16.8, 74.26 -16.77, 74.36 -16.73, 74.45 -16.69, 74.55 -16.65, 74.64 -16.61, 74.74 -16.57, 74.83 -16.52, 74.93 -16.48, 75.02 -16.43, 75.12 -16.39, 75.21 -16.34, 75.31 -16.29, 75.41 -16.24, 75.5 -16.19, 75.6 -16.14, 75.69 -16.08, 75.79 -16.03, 75.88 -15.97, 75.98 -15.92, 76.07 -15.86, 76.17 -15.8, 76.26 -15.74, 76.36 -15.68, 76.45 -15.61, 76.55 -15.55, 76.64 -15.48, 76.64 -21.19, 76.53 -21.23, 76.42 -21.27, 76.31 -21.31, 76.2 -21.34, 76.09 -21.38, 75.98 -21.42, 75.87 -21.46, 75.76 -21.49, 75.65 -21.53, 75.53 -21.56, 75.42 -21.59, 75.31 -21.63, 75.2 -21.66, 75.09 -21.69, 74.98 -21.72, 74.87 -21.75, 74.76 -21.78, 74.65 -21.81, 74.54 -21.84, 74.43 -21.86, 74.31 -21.89, 74.2 -21.92, 74.09 -21.94, 73.98 -21.97, 73.87 -21.99, 73.76 -22.01, 73.65 -22.04, 73.54 -22.06, 73.42 -22.08, 73.31 -22.1, 73.2 -22.12, 73.09 -22.14, 72.98 -22.16, 72.87 -22.18, 72.75 -22.19, 72.64 -22.21, 72.53 -22.23, 72.42 -22.24, 72.31 -22.26, 72.19 -22.27, 72.08 -22.28, 71.97 -22.3, 71.85 -22.31, 71.74 -22.32, 71.63 -22.33, 71.52 -22.34, 71.4 -22.35, 71.29 -22.36, 71.18 -22.37, 71.06 -22.37, 70.95 -22.38, 70.84 -22.38, 70.72 -22.39, 70.61 -22.39, 70.49 -22.4, 70.38 -22.4, 70.27 -22.4, 70.15 -22.4, 70.04 -22.41, 69.92 -22.41, 69.53 -22.4, 69.15 -22.39, 68.77 -22.38, 68.39 -22.35, 68.02 -22.32, 67.66 -22.28, 67.31 -22.24, 66.95 -22.19, 66.61 -22.13, 66.27 -22.07, 65.94 -22, 65.61 -21.92, 65.28 -21.84, 64.97 -21.74, 64.66 -21.65, 64.35 -21.54, 64.05 -21.43, 63.76 -21.31, 63.47 -21.19, 63.19 -21.06, 62.91 -20.92, 62.64 -20.77, 62.37 -20.62, 62.11 -20.46, 61.86 -20.3, 61.61 -20.12, 61.37 -19.94, 61.13 -19.76, 60.9 -19.57, 60.67 -19.37, 60.45 -19.16, 60.24 -18.95, 60.04 -18.74, 59.84 -18.51, 59.65 -18.29, 59.47 -18.06, 59.29 -17.82, 59.13 -17.58, 58.97 -17.33, 58.81 -17.08, 58.67 -16.82, 58.53 -16.56, 58.4 -16.29, 58.28 -16.02, 58.16 -15.74, 58.06 -15.45, 57.96 -15.16, 57.86 -14.87, 57.78 -14.57, 57.7 -14.26, 57.63 -13.95, 57.57 -13.64, 57.51 -13.32, 57.46 -12.99, 57.42 -12.66, 57.39 -12.32, 57.36 -11.98, 57.34 -11.63, 57.33 -11.28, 57.33 -10.92)),Polygon ((1.72 -6.56, 1.72 -6.35, 1.73 -6.15, 1.74 -5.94, 1.76 -5.74, 1.78 -5.54, 1.8 -5.35, 1.83 -5.15, 1.87 -4.96, 1.91 -4.77, 1.95 -4.59, 2 -4.41, 2.06 -4.22, 2.12 -4.05, 2.18 -3.87, 2.25 -3.7, 2.32 -3.53, 2.4 -3.36, 2.48 -3.2, 2.57 -3.03, 2.66 -2.88, 2.76 -2.72, 2.86 -2.56, 2.96 -2.41, 3.07 -2.26, 3.19 -2.12, 3.31 -1.97, 3.43 -1.83, 3.56 -1.69, 3.7 -1.56, 3.84 -1.42, 3.98 -1.29, 4.12 -1.17, 4.27 -1.04, 4.42 -0.93, 4.58 -0.82, 4.73 -0.71, 4.89 -0.6, 5.06 -0.5, 5.22 -0.41, 5.39 -0.32, 5.56 -0.23, 5.74 -0.15, 5.91 -0.07, 6.09 0, 6.28 0.07, 6.46 0.13, 6.65 0.19, 6.84 0.24, 7.03 0.3, 7.23 0.34, 7.43 0.38, 7.63 0.42, 7.84 0.45, 8.05 0.48, 8.26 0.51, 8.47 0.53, 8.69 0.54, 8.91 0.55, 9.13 0.56, 9.36 0.56, 9.53 0.56, 9.69 0.56, 9.85 0.55, 10.02 0.55, 10.18 0.54, 10.34 0.53, 10.49 0.51, 10.65 0.5, 10.8 0.48, 10.95 0.46, 11.1 0.44, 11.25 0.42, 11.4 0.39, 11.54 0.37, 11.69 0.34, 11.83 0.3, 11.97 0.27, 12.11 0.24, 12.24 0.2, 12.38 0.16, 12.51 0.12, 12.64 0.08, 12.77 0.03, 12.9 -0.02, 13.03 -0.07, 13.15 -0.12, 13.27 -0.17, 13.4 -0.23, 13.51 -0.28, 13.63 -0.34, 13.75 -0.41, 13.87 -0.47, 13.98 -0.54, 14.1 -0.6, 14.21 -0.68, 14.32 -0.75, 14.43 -0.83, 14.55 -0.9, 14.66 -0.99, 14.77 -1.07, 14.87 -1.16, 14.98 -1.24, 15.09 -1.33, 15.2 -1.43, 15.3 -1.52, 15.41 -1.62, 15.51 -1.72, 15.62 -1.83, 15.72 -1.93, 15.82 -2.04, 15.92 -2.15, 16.02 -2.26, 16.12 -2.38, 16.22 -2.49, 16.32 -2.61, 16.42 -2.74, 16.51 -2.86, 16.61 -2.99, 16.7 -3.12, 16.8 -3.25, 16.8 0, 23.84 0, 23.84 -12.48, 23.84 -12.83, 23.83 -13.17, 23.82 -13.51, 23.8 -13.83, 23.77 -14.15, 23.74 -14.47, 23.7 -14.78, 23.66 -15.08, 23.61 -15.37, 23.55 -15.66, 23.49 -15.94, 23.42 -16.22, 23.35 -16.49, 23.27 -16.75, 23.19 -17.01, 23.1 -17.26, 23 -17.5, 22.9 -17.74, 22.79 -17.97, 22.68 -18.19, 22.56 -18.41, 22.43 -18.62, 22.3 -18.82, 22.16 -19.02, 22.02 -19.21, 21.87 -19.4, 21.72 -19.57, 21.56 -19.75, 21.39 -19.91, 21.22 -20.07, 21.04 -20.22, 20.85 -20.37, 20.66 -20.51, 20.46 -20.65, 20.25 -20.78, 20.04 -20.91, 19.81 -21.03, 19.58 -21.15, 19.35 -21.26, 19.1 -21.37, 18.85 -21.47, 18.59 -21.57, 18.32 -21.66, 18.05 -21.74, 17.77 -21.82, 17.48 -21.9, 17.19 -21.97, 16.88 -22.03, 16.57 -22.09, 16.25 -22.15, 15.93 -22.2, 15.6 -22.24, 15.26 -22.28, 14.91 -22.31, 14.55 -22.34, 14.19 -22.36, 13.82 -22.38, 13.45 -22.4, 13.06 -22.4, 12.67 -22.41, 12.52 -22.41, 12.37 -22.41, 12.22 -22.4, 12.07 -22.4, 11.92 -22.4, 11.77 -22.4, 11.61 -22.39, 11.46 -22.39, 11.31 -22.38, 11.16 -22.38, 11.01 -22.37, 10.86 -22.36, 10.71 -22.35, 10.56 -22.34, 10.41 -22.34, 10.26 -22.33, 10.1 -22.32, 9.95 -22.31, 9.8 -22.29, 9.65 -22.28, 9.5 -22.27, 9.35 -22.26, 9.2 -22.24, 9.05 -22.23, 8.9 -22.21, 8.74 -22.2, 8.59 -22.18, 8.44 -22.16, 8.29 -22.14, 8.14 -22.13, 7.99 -22.11, 7.84 -22.09, 7.69 -22.07, 7.54 -22.05, 7.39 -22.02, 7.24 -22, 7.09 -21.98, 6.93 -21.96, 6.78 -21.93, 6.63 -21.91, 6.48 -21.88, 6.33 -21.86, 6.18 -21.83, 6.03 -21.8, 5.88 -21.78, 5.73 -21.75, 5.58 -21.72, 5.43 -21.69, 5.28 -21.66, 5.13 -21.63, 4.98 -21.6, 4.83 -21.57, 4.69 -21.54, 4.54 -21.51, 4.39 -21.47, 4.24 -21.44, 4.09 -21.4, 3.94 -21.37, 3.79 -21.33, 3.64 -21.3, 3.64 -15.95, 3.75 -16.01, 3.86 -16.07, 3.97 -16.13, 4.09 -16.19, 4.2 -16.24, 4.31 -16.3, 4.43 -16.35, 4.54 -16.4, 4.66 -16.46, 4.78 -16.51, 4.89 -16.56, 5.01 -16.6, 5.13 -16.65, 5.25 -16.7, 5.37 -16.74, 5.49 -16.79, 5.61 -16.83, 5.73 -16.87, 5.85 -16.92, 5.97 -16.96, 6.09 -17, 6.22 -17.03, 6.34 -17.07, 6.47 -17.11, 6.59 -17.14, 6.72 -17.18, 6.84 -17.21, 6.97 -17.24, 7.1 -17.27, 7.23 -17.3, 7.36 -17.33, 7.49 -17.36, 7.62 -17.39, 7.75 -17.42, 7.88 -17.44, 8.01 -17.47, 8.14 -17.49, 8.28 -17.51, 8.41 -17.53, 8.55 -17.55, 8.68 -17.57, 8.82 -17.59, 8.96 -17.61, 9.1 -17.62, 9.24 -17.64, 9.38 -17.65, 9.52 -17.67, 9.66 -17.68, 9.8 -17.69, 9.94 -17.7, 10.09 -17.71, 10.23 -17.72, 10.37 -17.73, 10.52 -17.73, 10.67 -17.74, 10.81 -17.74, 10.96 -17.75, 11.11 -17.75, 11.26 -17.75, 11.41 -17.75, 11.59 -17.75, 11.77 -17.75, 11.95 -17.74, 12.12 -17.74, 12.29 -17.73, 12.46 -17.72, 12.62 -17.71, 12.78 -17.7, 12.94 -17.68, 13.1 -17.66, 13.25 -17.65, 13.4 -17.63, 13.54 -17.61, 13.68 -17.58, 13.82 -17.56, 13.95 -17.53, 14.08 -17.5, 14.21 -17.47, 14.34 -17.44, 14.46 -17.41, 14.58 -17.37, 14.69 -17.34, 14.8 -17.3, 14.91 -17.26, 15.02 -17.22, 15.12 -17.17, 15.22 -17.13, 15.31 -17.08, 15.4 -17.03, 15.49 -16.98, 15.58 -16.93, 15.66 -16.88, 15.74 -16.82, 15.82 -16.76, 15.89 -16.7, 15.96 -16.64, 16.03 -16.58, 16.1 -16.51, 16.16 -16.44, 16.22 -16.37, 16.27 -16.3, 16.33 -16.23, 16.38 -16.15, 16.43 -16.07, 16.47 -15.99, 16.51 -15.91, 16.55 -15.83, 16.59 -15.74, 16.62 -15.65, 16.65 -15.56, 16.68 -15.47, 16.7 -15.37, 16.73 -15.28, 16.74 -15.18, 16.76 -15.08, 16.77 -14.98, 16.78 -14.87, 16.79 -14.77, 16.8 -14.66, 16.8 -14.55, 16.8 -14, 12.67 -14, 12.3 -14, 11.93 -13.99, 11.57 -13.98, 11.22 -13.97, 10.87 -13.95, 10.53 -13.93, 10.2 -13.9, 9.87 -13.87, 9.55 -13.84, 9.24 -13.8, 8.93 -13.76, 8.63 -13.71, 8.33 -13.66, 8.05 -13.61, 7.77 -13.55, 7.49 -13.49, 7.23 -13.42, 6.97 -13.35, 6.71 -13.28, 6.47 -13.2, 6.23 -13.12, 5.99 -13.03, 5.77 -12.94, 5.55 -12.85, 5.33 -12.75, 5.13 -12.65, 4.93 -12.54, 4.73 -12.43, 4.55 -12.32, 4.37 -12.2, 4.19 -12.08, 4.03 -11.95, 3.86 -11.82, 3.71 -11.69, 3.56 -11.55, 3.41 -11.4, 3.28 -11.25, 3.14 -11.1, 3.02 -10.94, 2.9 -10.78, 2.78 -10.61, 2.67 -10.44, 2.57 -10.26, 2.47 -10.08, 2.38 -9.89, 2.3 -9.7, 2.22 -9.51, 2.14 -9.31, 2.07 -9.11, 2.01 -8.9, 1.96 -8.68, 1.91 -8.47, 1.86 -8.24, 1.82 -8.02, 1.79 -7.79, 1.77 -7.55, 1.75 -7.31, 1.73 -7.07, 1.72 -6.82, 1.72 -6.56),(8.77 -6.92, 8.77 -7.02, 8.77 -7.11, 8.78 -7.2, 8.79 -7.29, 8.8 -7.38, 8.81 -7.47, 8.83 -7.55, 8.84 -7.64, 8.87 -7.72, 8.89 -7.8, 8.91 -7.88, 8.94 -7.96, 8.97 -8.04, 9.01 -8.11, 9.04 -8.19, 9.08 -8.26, 9.12 -8.33, 9.17 -8.4, 9.21 -8.46, 9.26 -8.53, 9.31 -8.59, 9.36 -8.66, 9.42 -8.72, 9.48 -8.78, 9.54 -8.83, 9.6 -8.89, 9.66 -8.95, 9.73 -9, 9.8 -9.05, 9.88 -9.1, 9.95 -9.15, 10.03 -9.2, 10.11 -9.24, 10.19 -9.29, 10.27 -9.33, 10.36 -9.37, 10.45 -9.41, 10.54 -9.44, 10.64 -9.48, 10.73 -9.51, 10.83 -9.55, 10.94 -9.58, 11.04 -9.61, 11.15 -9.63, 11.25 -9.66, 11.37 -9.68, 11.48 -9.7, 11.59 -9.72, 11.71 -9.74, 11.83 -9.76, 11.96 -9.78, 12.08 -9.79, 12.21 -9.8, 12.34 -9.81, 12.47 -9.82, 12.61 -9.83, 12.75 -9.84, 12.88 -9.84, 13.03 -9.84, 13.17 -9.84, 16.8 -9.84, 16.8 -9.05, 16.8 -8.91, 16.79 -8.77, 16.78 -8.63, 16.77 -8.49, 16.76 -8.36, 16.74 -8.23, 16.72 -8.09, 16.7 -7.96, 16.68 -7.84, 16.65 -7.71, 16.62 -7.58, 16.58 -7.46, 16.54 -7.34, 16.5 -7.22, 16.46 -7.1, 16.41 -6.98, 16.37 -6.86, 16.31 -6.75, 16.26 -6.64, 16.2 -6.53, 16.14 -6.42, 16.07 -6.31, 16.01 -6.2, 15.94 -6.1, 15.86 -5.99, 15.79 -5.89, 15.71 -5.79, 15.63 -5.69, 15.54 -5.6, 15.45 -5.5, 15.36 -5.41, 15.27 -5.32, 15.18 -5.23, 15.08 -5.15, 14.99 -5.07, 14.89 -4.99, 14.79 -4.91, 14.69 -4.84, 14.59 -4.77, 14.49 -4.71, 14.38 -4.65, 14.28 -4.59, 14.17 -4.53, 14.06 -4.48, 13.95 -4.43, 13.84 -4.39, 13.72 -4.35, 13.61 -4.31, 13.49 -4.27, 13.38 -4.24, 13.26 -4.21, 13.14 -4.18, 13.02 -4.16, 12.89 -4.14, 12.77 -4.12, 12.64 -4.1, 12.52 -4.09, 12.39 -4.08, 12.26 -4.08, 12.13 -4.08, 12.02 -4.08, 11.92 -4.08, 11.82 -4.09, 11.72 -4.09, 11.62 -4.1, 11.52 -4.11, 11.43 -4.12, 11.33 -4.13, 11.24 -4.15, 11.15 -4.16, 11.06 -4.18, 10.97 -4.2, 10.89 -4.22, 10.8 -4.24, 10.72 -4.27, 10.64 -4.29, 10.56 -4.32, 10.48 -4.35, 10.4 -4.38, 10.33 -4.41, 10.25 -4.45, 10.18 -4.49, 10.11 -4.52, 10.04 -4.56, 9.97 -4.6, 9.91 -4.65, 9.84 -4.69, 9.78 -4.74, 9.72 -4.79, 9.66 -4.84, 9.6 -4.89, 9.54 -4.94, 9.49 -4.99, 9.43 -5.05, 9.38 -5.1, 9.34 -5.16, 9.29 -5.22, 9.24 -5.28, 9.2 -5.34, 9.16 -5.4, 9.12 -5.47, 9.09 -5.53, 9.05 -5.6, 9.02 -5.67, 8.99 -5.74, 8.96 -5.81, 8.93 -5.88, 8.91 -5.95, 8.89 -6.02, 8.86 -6.1, 8.85 -6.18, 8.83 -6.25, 8.81 -6.33, 8.8 -6.41, 8.79 -6.5, 8.78 -6.58, 8.77 -6.66, 8.77 -6.75, 8.77 -6.83, 8.77 -6.92)),Polygon ((30.34 -30.39, 30.34 0, 37.34 0, 37.34 -3.17, 37.44 -3.04, 37.54 -2.92, 37.63 -2.8, 37.73 -2.68, 37.83 -2.56, 37.93 -2.44, 38.03 -2.33, 38.13 -2.22, 38.24 -2.11, 38.34 -2, 38.44 -1.9, 38.55 -1.8, 38.65 -1.7, 38.76 -1.6, 38.86 -1.5, 38.97 -1.41, 39.08 -1.32, 39.18 -1.23, 39.29 -1.14, 39.4 -1.06, 39.51 -0.98, 39.62 -0.9, 39.74 -0.82, 39.85 -0.75, 39.96 -0.67, 40.08 -0.6, 40.19 -0.54, 40.31 -0.47, 40.42 -0.41, 40.54 -0.34, 40.66 -0.28, 40.78 -0.23, 40.9 -0.17, 41.02 -0.12, 41.14 -0.07, 41.26 -0.02, 41.39 0.03, 41.51 0.08, 41.64 0.12, 41.77 0.16, 41.9 0.2, 42.03 0.24, 42.16 0.27, 42.29 0.3, 42.43 0.34, 42.56 0.37, 42.7 0.39, 42.84 0.42, 42.98 0.44, 43.12 0.46, 43.26 0.48, 43.4 0.5, 43.54 0.51, 43.69 0.53, 43.83 0.54, 43.98 0.55, 44.13 0.55, 44.28 0.56, 44.43 0.56, 44.58 0.56, 44.85 0.56, 45.11 0.55, 45.37 0.53, 45.63 0.51, 45.89 0.47, 46.14 0.43, 46.39 0.39, 46.63 0.33, 46.88 0.27, 47.12 0.21, 47.35 0.13, 47.58 0.05, 47.81 -0.04, 48.04 -0.14, 48.26 -0.24, 48.48 -0.35, 48.7 -0.47, 48.91 -0.59, 49.12 -0.73, 49.33 -0.86, 49.54 -1.01, 49.74 -1.16, 49.93 -1.32, 50.13 -1.49, 50.32 -1.67, 50.51 -1.85, 50.69 -2.04, 50.87 -2.23, 51.05 -2.44, 51.23 -2.65, 51.4 -2.86, 51.56 -3.08, 51.72 -3.31, 51.87 -3.54, 52.02 -3.77, 52.16 -4.01, 52.3 -4.25, 52.43 -4.49, 52.55 -4.74, 52.67 -4.99, 52.78 -5.25, 52.89 -5.51, 52.99 -5.78, 53.09 -6.05, 53.18 -6.32, 53.26 -6.6, 53.34 -6.88, 53.41 -7.17, 53.48 -7.46, 53.54 -7.75, 53.59 -8.05, 53.64 -8.35, 53.69 -8.66, 53.72 -8.97, 53.76 -9.29, 53.78 -9.6, 53.8 -9.93, 53.82 -10.26, 53.83 -10.59, 53.83 -10.92, 53.83 -11.26, 53.82 -11.59, 53.8 -11.92, 53.78 -12.24, 53.76 -12.56, 53.72 -12.87, 53.69 -13.18, 53.64 -13.49, 53.59 -13.79, 53.54 -14.09, 53.48 -14.39, 53.41 -14.68, 53.34 -14.96, 53.26 -15.24, 53.18 -15.52, 53.09 -15.8, 52.99 -16.06, 52.89 -16.33, 52.78 -16.59, 52.67 -16.85, 52.55 -17.1, 52.43 -17.35, 52.3 -17.6, 52.16 -17.84, 52.02 -18.07, 51.87 -18.31, 51.72 -18.53, 51.56 -18.76, 51.4 -18.98, 51.23 -19.2, 51.05 -19.41, 50.87 -19.61, 50.69 -19.81, 50.51 -19.99, 50.32 -20.18, 50.13 -20.35, 49.93 -20.52, 49.74 -20.68, 49.54 -20.83, 49.33 -20.98, 49.12 -21.12, 48.91 -21.25, 48.7 -21.38, 48.48 -21.49, 48.26 -21.6, 48.04 -21.71, 47.81 -21.8, 47.58 -21.89, 47.35 -21.97, 47.12 -22.05, 46.88 -22.12, 46.63 -22.18, 46.39 -22.23, 46.14 -22.28, 45.89 -22.32, 45.63 -22.35, 45.37 -22.37, 45.11 -22.39, 44.85 -22.4, 44.58 -22.41, 44.43 -22.41, 44.28 -22.4, 44.13 -22.4, 43.98 -22.39, 43.83 -22.38, 43.69 -22.37, 43.54 -22.36, 43.4 -22.34, 43.26 -22.32, 43.12 -22.31, 42.98 -22.28, 42.84 -22.26, 42.7 -22.24, 42.56 -22.21, 42.43 -22.18, 42.29 -22.15, 42.16 -22.12, 42.03 -22.08, 41.9 -22.04, 41.77 -22, 41.64 -21.96, 41.51 -21.92, 41.39 -21.87, 41.26 -21.83, 41.14 -21.78, 41.02 -21.73, 40.9 -21.67, 40.78 -21.62, 40.66 -21.56, 40.54 -21.5, 40.42 -21.44, 40.31 -21.37, 40.19 -21.31, 40.08 -21.24, 39.96 -21.17, 39.85 -21.1, 39.74 -21.02, 39.62 -20.94, 39.51 -20.86, 39.4 -20.78, 39.29 -20.7, 39.18 -20.61, 39.08 -20.52, 38.97 -20.43, 38.86 -20.34, 38.76 -20.24, 38.65 -20.15, 38.55 -20.05, 38.44 -19.94, 38.34 -19.84, 38.24 -19.73, 38.13 -19.62, 38.03 -19.51, 37.93 -19.4, 37.83 -19.28, 37.73 -19.17, 37.63 -19.05, 37.54 -18.92, 37.44 -18.8, 37.34 -18.67, 37.34 -30.39, 30.34 -30.39),(37.34 -10.92, 37.35 -11.13, 37.35 -11.33, 37.36 -11.53, 37.37 -11.73, 37.38 -11.92, 37.39 -12.11, 37.41 -12.29, 37.43 -12.48, 37.45 -12.66, 37.48 -12.83, 37.5 -13.01, 37.54 -13.18, 37.57 -13.34, 37.6 -13.51, 37.64 -13.67, 37.68 -13.82, 37.73 -13.98, 37.77 -14.13, 37.82 -14.27, 37.88 -14.42, 37.93 -14.56, 37.99 -14.7, 38.05 -14.83, 38.11 -14.96, 38.17 -15.09, 38.24 -15.21, 38.31 -15.34, 38.39 -15.45, 38.46 -15.57, 38.54 -15.68, 38.62 -15.79, 38.7 -15.89, 38.79 -15.99, 38.88 -16.09, 38.97 -16.18, 39.06 -16.27, 39.15 -16.36, 39.25 -16.44, 39.35 -16.52, 39.45 -16.6, 39.56 -16.67, 39.66 -16.73, 39.77 -16.8, 39.88 -16.86, 40 -16.92, 40.11 -16.97, 40.23 -17.02, 40.35 -17.06, 40.48 -17.11, 40.6 -17.14, 40.73 -17.18, 40.86 -17.21, 40.99 -17.24, 41.13 -17.26, 41.26 -17.28, 41.4 -17.3, 41.54 -17.31, 41.69 -17.32, 41.84 -17.33, 41.98 -17.33, 42.13 -17.33, 42.28 -17.32, 42.42 -17.31, 42.57 -17.3, 42.7 -17.28, 42.84 -17.26, 42.98 -17.24, 43.11 -17.21, 43.24 -17.18, 43.37 -17.15, 43.49 -17.11, 43.61 -17.07, 43.73 -17.02, 43.85 -16.97, 43.97 -16.92, 44.08 -16.86, 44.19 -16.8, 44.3 -16.74, 44.41 -16.67, 44.51 -16.6, 44.61 -16.52, 44.71 -16.45, 44.81 -16.36, 44.9 -16.28, 44.99 -16.19, 45.08 -16.1, 45.17 -16, 45.25 -15.9, 45.33 -15.8, 45.41 -15.69, 45.49 -15.58, 45.57 -15.46, 45.64 -15.34, 45.71 -15.22, 45.77 -15.1, 45.84 -14.97, 45.9 -14.84, 45.96 -14.71, 46.02 -14.57, 46.07 -14.43, 46.12 -14.28, 46.17 -14.14, 46.21 -13.99, 46.26 -13.83, 46.3 -13.68, 46.34 -13.52, 46.37 -13.35, 46.41 -13.18, 46.44 -13.01, 46.46 -12.84, 46.49 -12.66, 46.51 -12.48, 46.53 -12.3, 46.55 -12.11, 46.56 -11.92, 46.57 -11.73, 46.58 -11.53, 46.59 -11.33, 46.59 -11.13, 46.59 -10.92, 46.59 -10.72, 46.59 -10.51, 46.58 -10.31, 46.57 -10.11, 46.56 -9.92, 46.55 -9.73, 46.53 -9.54, 46.51 -9.36, 46.49 -9.18, 46.46 -9, 46.44 -8.83, 46.41 -8.66, 46.37 -8.49, 46.34 -8.33, 46.3 -8.17, 46.26 -8.01, 46.21 -7.86, 46.17 -7.71, 46.12 -7.56, 46.07 -7.41, 46.02 -7.27, 45.96 -7.14, 45.9 -7, 45.84 -6.87, 45.77 -6.74, 45.71 -6.62, 45.64 -6.5, 45.57 -6.38, 45.49 -6.27, 45.41 -6.16, 45.33 -6.05, 45.25 -5.94, 45.17 -5.84, 45.08 -5.75, 44.99 -5.65, 44.9 -5.57, 44.81 -5.48, 44.71 -5.4, 44.61 -5.32, 44.51 -5.24, 44.41 -5.17, 44.3 -5.11, 44.19 -5.04, 44.08 -4.98, 43.97 -4.93, 43.85 -4.87, 43.73 -4.82, 43.61 -4.78, 43.49 -4.74, 43.37 -4.7, 43.24 -4.66, 43.11 -4.63, 42.98 -4.6, 42.84 -4.58, 42.7 -4.56, 42.57 -4.54, 42.42 -4.53, 42.28 -4.52, 42.13 -4.52, 41.98 -4.52, 41.84 -4.52, 41.69 -4.52, 41.54 -4.53, 41.4 -4.54, 41.26 -4.56, 41.13 -4.58, 40.99 -4.61, 40.86 -4.63, 40.73 -4.66, 40.6 -4.7, 40.48 -4.74, 40.35 -4.78, 40.23 -4.83, 40.11 -4.87, 40 -4.93, 39.88 -4.98, 39.77 -5.04, 39.66 -5.11, 39.56 -5.18, 39.45 -5.25, 39.35 -5.32, 39.25 -5.4, 39.15 -5.48, 39.06 -5.57, 38.97 -5.66, 38.88 -5.75, 38.79 -5.85, 38.7 -5.95, 38.62 -6.06, 38.54 -6.16, 38.46 -6.28, 38.39 -6.39, 38.31 -6.51, 38.24 -6.63, 38.17 -6.75, 38.11 -6.88, 38.05 -7.01, 37.99 -7.15, 37.93 -7.28, 37.88 -7.43, 37.82 -7.57, 37.77 -7.72, 37.73 -7.87, 37.68 -8.02, 37.64 -8.18, 37.6 -8.34, 37.57 -8.5, 37.54 -8.67, 37.5 -8.84, 37.48 -9.01, 37.45 -9.19, 37.43 -9.37, 37.41 -9.55, 37.39 -9.74, 37.38 -9.93, 37.37 -10.12, 37.36 -10.31, 37.35 -10.51, 37.35 -10.72, 37.34 -10.92)))", ) self.assertEqual(device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth), 74) diff --git a/tests/src/python/test_qgsnurbscurve.py b/tests/src/python/test_qgsnurbscurve.py new file mode 100644 index 000000000000..f66372574be6 --- /dev/null +++ b/tests/src/python/test_qgsnurbscurve.py @@ -0,0 +1,374 @@ +"""QGIS Unit tests for QgsNurbsCurve. + +.. 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__ = "Loic Bartoletti" +__date__ = "10/09/2025" +__copyright__ = "Copyright 2025, The QGIS Project" + + +import qgis # NOQA + +from qgis.core import ( + QgsNurbsCurve, + QgsCurve, + QgsPoint, + QgsLineString, + QgsGeometry, + QgsWkbTypes, + QgsVertexId, + Qgis, +) +from qgis.PyQt.QtCore import QByteArray +import binascii + +from qgis.testing import start_app, unittest +from utilities import unitTestDataPath + +start_app() + + +class TestQgsNurbsCurve(unittest.TestCase): + + def testConstructor(self): + """Test QgsNurbsCurve constructor""" + # Test empty constructor + curve = QgsNurbsCurve() + self.assertEqual(curve.numPoints(), 0) + self.assertEqual(curve.degree(), 0) + self.assertEqual(len(curve.controlPoints()), 0) + self.assertEqual(len(curve.knots()), 0) + self.assertEqual(len(curve.weights()), 0) + + # Test constructor with parameters + control_points = [ + QgsPoint(0, 0), + QgsPoint(1, 1), + QgsPoint(2, 0), + QgsPoint(3, 1), + ] + degree = 2 + knots = [0, 0, 0, 1, 2, 2, 2] # degree + 1 + control_points = 2 + 1 + 4 = 7 + weights = [1, 1, 1, 1] + + nurbs = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertEqual(nurbs.degree(), 2) + self.assertEqual(len(nurbs.controlPoints()), 4) + self.assertEqual(len(nurbs.knots()), 7) + self.assertEqual(len(nurbs.weights()), 4) + + def testClone(self): + """Test clone method""" + # Test with a simple linear case first + control_points = [QgsPoint(0, 0), QgsPoint(10, 10)] + degree = 1 + knots = [0, 0, 1, 1] # Simple linear knot vector + weights = [1, 1] + + original = QgsNurbsCurve(control_points, degree, knots, weights) + cloned = original.clone() + + self.assertIsInstance(cloned, QgsCurve) + self.assertEqual(cloned.geometryType(), "NurbsCurve") + + if not original.isEmpty() and not cloned.isEmpty(): + self.assertAlmostEqual(original.length(), cloned.length(), places=5) + + def testGeometryType(self): + """Test geometry type""" + curve = QgsNurbsCurve() + self.assertEqual(curve.geometryType(), "NurbsCurve") + + def testProperties(self): + """Test NURBS curve properties""" + # Test B-spline (all weights = 1) + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + bspline = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertTrue(bspline.isBSpline()) + self.assertFalse(bspline.isRational()) + + # Test rational curve (non-uniform weights) + rational_weights = [1, 2, 1] + rational = QgsNurbsCurve(control_points, degree, knots, rational_weights) + self.assertFalse(rational.isBSpline()) + self.assertTrue(rational.isRational()) + + def testBezierCurve(self): + """Test Bézier curve detection""" + # Cubic Bézier curve + control_points = [ + QgsPoint(0, 0), + QgsPoint(1, 2), + QgsPoint(2, 2), + QgsPoint(3, 0), + ] + degree = 3 + knots = [0, 0, 0, 0, 1, 1, 1, 1] # Bézier knot vector + weights = [1, 1, 1, 1] + + bezier = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertTrue(bezier.isBezier()) + + def testEvaluation(self): + """Test curve evaluation at parameter t""" + # Simple linear case (degree 1) + control_points = [QgsPoint(0, 0), QgsPoint(10, 10)] + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + + linear = QgsNurbsCurve(control_points, degree, knots, weights) + + # Test evaluation at different parameters + start_point = linear.evaluate(0.0) + self.assertEqual(start_point.x(), 0.0) + self.assertEqual(start_point.y(), 0.0) + + mid_point = linear.evaluate(0.5) + self.assertEqual(mid_point.x(), 5.0) + self.assertEqual(mid_point.y(), 5.0) + + end_point = linear.evaluate(1.0) + self.assertEqual(end_point.x(), 10.0) + self.assertEqual(end_point.y(), 10.0) + + def testStartEndPoints(self): + """Test start and end points""" + # Simple linear case + control_points = [QgsPoint(0, 0), QgsPoint(10, 5)] + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + + start = curve.startPoint() + end = curve.endPoint() + + # For a linear NURBS curve, start/end should be at first/last control points + self.assertAlmostEqual(start.x(), 0.0, places=5) + self.assertAlmostEqual(start.y(), 0.0, places=5) + self.assertAlmostEqual(end.x(), 10.0, places=5) + self.assertAlmostEqual(end.y(), 5.0, places=5) + + def testNumPoints(self): + """Test number of points""" + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + # numPoints might return discretized points or control points count + # depending on implementation + self.assertGreater(curve.numPoints(), 0) + + def testToLineString(self): + """Test conversion to line string""" + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + line_string = curve.curveToLine() + + self.assertIsInstance(line_string, QgsLineString) + self.assertGreater(line_string.numPoints(), 2) + + # Check that start and end points match + self.assertEqual(line_string.startPoint().x(), curve.startPoint().x()) + self.assertEqual(line_string.startPoint().y(), curve.startPoint().y()) + self.assertEqual(line_string.endPoint().x(), curve.endPoint().x()) + self.assertEqual(line_string.endPoint().y(), curve.endPoint().y()) + + def testCurveToLine(self): + """Test curveToLine method""" + control_points = [QgsPoint(0, 0), QgsPoint(5, 5), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + line = curve.curveToLine() + + self.assertIsInstance(line, QgsLineString) + self.assertGreater(line.numPoints(), 2) + + def testReversed(self): + """Test reversed curve""" + control_points = [QgsPoint(0, 0), QgsPoint(5, 5), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + original = QgsNurbsCurve(control_points, degree, knots, weights) + reversed_curve = original.reversed() + + # Start and end should be swapped + self.assertEqual(original.startPoint().x(), reversed_curve.endPoint().x()) + self.assertEqual(original.startPoint().y(), reversed_curve.endPoint().y()) + self.assertEqual(original.endPoint().x(), reversed_curve.startPoint().x()) + self.assertEqual(original.endPoint().y(), reversed_curve.startPoint().y()) + + def testBoundingBox(self): + """Test bounding box calculation""" + # Quadratic Bézier curve: the curve reaches max y=5 at t=0.5 + # The control points define the convex hull, but the curve doesn't pass through them + control_points = [QgsPoint(0, 0), QgsPoint(5, 10), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + bbox = curve.boundingBox() + + self.assertLessEqual(bbox.xMinimum(), 0.0) + self.assertGreaterEqual(bbox.xMaximum(), 10.0) + self.assertLessEqual(bbox.yMinimum(), 0.0) + # For a quadratic Bézier with these control points, max y is 5.0 (at t=0.5) + self.assertGreaterEqual(bbox.yMaximum(), 4.9) # Allow small tolerance + + def testEquals(self): + """Test equality comparison""" + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve1 = QgsNurbsCurve(control_points, degree, knots, weights) + curve2 = QgsNurbsCurve(control_points, degree, knots, weights) + curve3 = QgsNurbsCurve( + [QgsPoint(0, 0), QgsPoint(2, 2)], 1, [0, 0, 1, 1], [1, 1] + ) + + self.assertTrue(curve1.equals(curve2)) + self.assertFalse(curve1.equals(curve3)) + + def testIsEmpty(self): + """Test isEmpty method""" + empty_curve = QgsNurbsCurve() + self.assertTrue(empty_curve.isEmpty()) + + control_points = [QgsPoint(0, 0), QgsPoint(1, 1)] + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + curve = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertFalse(curve.isEmpty()) + + def testLength(self): + """Test curve length calculation""" + # Simple linear case + control_points = [QgsPoint(0, 0), QgsPoint(3, 4)] # 3-4-5 triangle + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + + linear = QgsNurbsCurve(control_points, degree, knots, weights) + length = linear.length() + self.assertAlmostEqual(length, 5.0, places=2) + + def testHasCurvedSegments(self): + """Test hasCurvedSegments method""" + # Any NURBS curve with degree > 1 should have curved segments + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertTrue(curve.hasCurvedSegments()) + + def testDimension(self): + """Test dimension method""" + curve = QgsNurbsCurve() + self.assertEqual(curve.dimension(), 1) # Curves are 1-dimensional + + def testEvaluateExceptions(self): + """Test that evaluate() raises ValueError for invalid parameter t""" + control_points = [QgsPoint(0, 0), QgsPoint(10, 10)] + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + + # Test t < 0 raises ValueError + with self.assertRaises(ValueError): + curve.evaluate(-0.1) + + # Test t > 1 raises ValueError + with self.assertRaises(ValueError): + curve.evaluate(1.1) + + # Test boundary values are valid + start = curve.evaluate(0.0) + self.assertIsNotNone(start) + end = curve.evaluate(1.0) + self.assertIsNotNone(end) + + def testWeightExceptions(self): + """Test that weight() raises IndexError for invalid index""" + control_points = [QgsPoint(0, 0), QgsPoint(5, 5), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 2, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + + # Test valid indices work + self.assertEqual(curve.weight(0), 1.0) + self.assertEqual(curve.weight(1), 2.0) + self.assertEqual(curve.weight(2), 1.0) + + # Test negative index raises IndexError + with self.assertRaises(IndexError): + curve.weight(-1) + + # Test index >= count raises IndexError + with self.assertRaises(IndexError): + curve.weight(3) + + with self.assertRaises(IndexError): + curve.weight(100) + + def testSetWeightExceptions(self): + """Test that setWeight() raises exceptions for invalid parameters""" + control_points = [QgsPoint(0, 0), QgsPoint(5, 5), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + + # Test valid setWeight works + curve.setWeight(1, 2.0) + self.assertEqual(curve.weight(1), 2.0) + + # Test negative index raises IndexError + with self.assertRaises(IndexError): + curve.setWeight(-1, 1.0) + + # Test index >= count raises IndexError + with self.assertRaises(IndexError): + curve.setWeight(3, 1.0) + + # Test weight <= 0 raises ValueError + with self.assertRaises(ValueError): + curve.setWeight(0, 0.0) + + with self.assertRaises(ValueError): + curve.setWeight(0, -1.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testdata/control_images/selective_masking/layout_export/layout_export_mask.png b/tests/testdata/control_images/selective_masking/layout_export/layout_export_mask.png index d4e104981d87..0ba23acd7199 100644 Binary files a/tests/testdata/control_images/selective_masking/layout_export/layout_export_mask.png and b/tests/testdata/control_images/selective_masking/layout_export/layout_export_mask.png differ diff --git a/tests/testdata/control_images/selective_masking/layout_export_2_sources_masking/layout_export_2_sources_masking_mask.png b/tests/testdata/control_images/selective_masking/layout_export_2_sources_masking/layout_export_2_sources_masking_mask.png index 8c28e9b731c7..0a1b6692d905 100644 Binary files a/tests/testdata/control_images/selective_masking/layout_export_2_sources_masking/layout_export_2_sources_masking_mask.png and b/tests/testdata/control_images/selective_masking/layout_export_2_sources_masking/layout_export_2_sources_masking_mask.png differ diff --git a/tests/testdata/control_images/selective_masking/layout_export_markerline_masked/layout_export_markerline_masked_mask.png b/tests/testdata/control_images/selective_masking/layout_export_markerline_masked/layout_export_markerline_masked_mask.png index baf61f161f53..ea3674bcf848 100644 Binary files a/tests/testdata/control_images/selective_masking/layout_export_markerline_masked/layout_export_markerline_masked_mask.png and b/tests/testdata/control_images/selective_masking/layout_export_markerline_masked/layout_export_markerline_masked_mask.png differ diff --git a/tests/testdata/control_images/selective_masking/layout_export_w_blend_mode/layout_export_w_blend_mode_mask.png b/tests/testdata/control_images/selective_masking/layout_export_w_blend_mode/layout_export_w_blend_mode_mask.png index 51940cf7cf73..e4bdc2e5e419 100644 Binary files a/tests/testdata/control_images/selective_masking/layout_export_w_blend_mode/layout_export_w_blend_mode_mask.png and b/tests/testdata/control_images/selective_masking/layout_export_w_blend_mode/layout_export_w_blend_mode_mask.png differ diff --git a/tests/testdata/control_images/selective_masking/layout_export_w_raster/layout_export_w_raster_mask.png b/tests/testdata/control_images/selective_masking/layout_export_w_raster/layout_export_w_raster_mask.png index f1601628c39d..d7cfd54ef9c5 100644 Binary files a/tests/testdata/control_images/selective_masking/layout_export_w_raster/layout_export_w_raster_mask.png and b/tests/testdata/control_images/selective_masking/layout_export_w_raster/layout_export_w_raster_mask.png differ