QGIS API Documentation 3.99.0-Master (7d2ca374f2d)
Loading...
Searching...
No Matches
qgsnurbscurve.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsnurbscurve.cpp
3 -----------------
4 begin : September 2025
5 copyright : (C) 2025 by Loïc Bartoletti
6 email : loic dot bartoletti at oslandia dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgsnurbscurve.h"
19
20#include <algorithm>
21#include <cmath>
22#include <limits>
23#include <memory>
24#include <nlohmann/json.hpp>
25#include <vector>
26
27#include "qgsapplication.h"
28#include "qgsbox3d.h"
30#include "qgsfeedback.h"
32#include "qgsgeometryutils.h"
34#include "qgslinestring.h"
35#include "qgspoint.h"
36#include "qgsrectangle.h"
37#include "qgswkbptr.h"
38#include "qgswkbtypes.h"
39
40#include <QPainterPath>
41#include <QString>
42
43using namespace Qt::StringLiterals;
44
45using namespace nlohmann;
46
51
52QgsNurbsCurve::QgsNurbsCurve( const QVector<QgsPoint> &controlPoints, int degree, const QVector<double> &knots, const QVector<double> &weights )
53 : mControlPoints( controlPoints )
54 , mKnots( knots )
55 , mWeights( weights )
56 , mDegree( degree )
57{
59
60 // Update WKB type based on coordinate dimensions
61 if ( !mControlPoints.isEmpty() )
62 {
63 const QgsPoint &firstPoint = mControlPoints.first();
64 if ( firstPoint.is3D() )
65 mWkbType = QgsWkbTypes::addZ( mWkbType );
66 if ( firstPoint.isMeasure() )
67 mWkbType = QgsWkbTypes::addM( mWkbType );
68 }
69}
70
72{
73 return new QgsNurbsCurve( *this );
74}
75
77{
78 const int n = mControlPoints.size();
79 if ( n < 2 || mDegree < 1 )
80 return false;
81
82 if ( mDegree != n - 1 )
83 return false;
84
85 if ( !isBSpline() )
86 return false;
87
88 if ( mKnots.size() != n + mDegree + 1 )
89 return false;
90
91 for ( int i = 0; i <= mDegree; ++i )
92 {
93 if ( !qgsDoubleNear( mKnots[i], 0.0 ) )
94 return false;
95 }
96 for ( int i = n; i < mKnots.size(); ++i )
97 {
98 if ( !qgsDoubleNear( mKnots[i], 1.0 ) )
99 return false;
100 }
101
102 return true;
103}
104
106{
107 for ( const double w : mWeights )
108 {
109 if ( !qgsDoubleNear( w, 1.0 ) )
110 return false;
111 }
112 return true;
113}
114
116{
117 return !isBSpline();
118}
119
121{
122 const int controlPointCount = mControlPoints.size();
123 const int degree = mDegree;
124
125 // Basic requirements: degree >= 1, at least degree+1 points,
126 // and the number of control points must satisfy (controlPointCount - 1) % degree == 0.
127 if ( degree < 1 || controlPointCount < degree + 1 || ( controlPointCount - 1 ) % degree != 0 )
128 return false;
129
130 const int segmentCount = ( controlPointCount - 1 ) / degree;
131 const int expectedKnotCount = controlPointCount + degree + 1;
132
133 if ( mKnots.size() != expectedKnotCount )
134 return false;
135
136 // 1. Check that the knot vector is non-decreasing
137 for ( int i = 1; i < expectedKnotCount; ++i )
138 {
139 if ( mKnots[i] < mKnots[i - 1] && !qgsDoubleNear( mKnots[i], mKnots[i - 1] ) )
140 return false;
141 }
142
143 // 2. Check clamping: first degree+1 knots must be 0.0
144 for ( int i = 0; i < degree + 1; ++i )
145 {
146 if ( !qgsDoubleNear( mKnots[i], 0.0 ) )
147 return false;
148 }
149
150 // 3. Check clamping: last degree+1 knots must be equal to the number of segments
151 const double lastKnotValue = static_cast<double>( segmentCount );
152 for ( int i = expectedKnotCount - ( degree + 1 ); i < expectedKnotCount; ++i )
153 {
154 if ( !qgsDoubleNear( mKnots[i], lastKnotValue ) )
155 return false;
156 }
157
158 // 4. Check interior knots: multiplicity 'degree' at each integer junction
159 for ( int segmentIndex = 1; segmentIndex < segmentCount; ++segmentIndex )
160 {
161 const double knotValue = static_cast<double>( segmentIndex );
162 const int startIndex = ( degree + 1 ) + ( segmentIndex - 1 ) * degree;
163 for ( int j = 0; j < degree; ++j )
164 {
165 if ( !qgsDoubleNear( mKnots[startIndex + j], knotValue ) )
166 return false;
167 }
168 }
169
170 return true;
171}
172
187static int findKnotSpan( const int degree, const double u, const QVector<double> &knots, const int nPoints )
188{
189 // Special case: u at or beyond end of parameter range
190 if ( u >= knots[nPoints] )
191 return nPoints - 1;
192
193 // Special case: u at or before start of parameter range
194 if ( u <= knots[degree] )
195 return degree;
196
197 // Binary search for the knot span
198 int low = degree;
199 int high = nPoints;
200 int mid = ( low + high ) / 2;
201
202 while ( u < knots[mid] || u >= knots[mid + 1] )
203 {
204 if ( u < knots[mid] )
205 high = mid;
206 else
207 low = mid;
208 mid = ( low + high ) / 2;
209 }
210
211 return mid;
212}
213
214// Evaluates the NURBS curve at parameter t using De Boor's algorithm.
215// Implements Algorithm A4.1 from "The NURBS Book" (Piegl & Tiller).
217{
218 const int n = mControlPoints.size();
219 if ( n == 0 )
220 {
221 return QgsPoint();
222 }
223
224 QString error;
225 if ( !isValid( error, Qgis::GeometryValidityFlags() ) )
226 {
227 return QgsPoint();
228 }
229
230 // Clamp parameter t to valid range [0,1]
231 if ( t <= 0.0 )
232 return mControlPoints.first();
233 if ( t >= 1.0 )
234 return mControlPoints.last();
235
236 const bool hasZ = !mControlPoints.isEmpty() && mControlPoints.first().is3D();
237 const bool hasM = !mControlPoints.isEmpty() && mControlPoints.first().isMeasure();
238
239 // Remap parameter from [0,1] to knot vector range [knots[degree], knots[n]]
240 const double u = mKnots[mDegree] + t * ( mKnots[n] - mKnots[mDegree] );
241
242 // Find the knot span containing parameter u (Algorithm A2.1)
243 const int span = findKnotSpan( mDegree, u, mKnots, n );
244
245 // Temporary arrays for De Boor iteration (degree+1 points)
246 // Using homogeneous coordinates: (w*x, w*y, w*z, w) for rational curves
247 // Use std::vector for local temp arrays - no need for Qt's implicit sharing overhead
248 std::vector<double> tempX( mDegree + 1 );
249 std::vector<double> tempY( mDegree + 1 );
250 std::vector<double> tempZ( mDegree + 1 );
251 std::vector<double> tempM( mDegree + 1 );
252 std::vector<double> tempW( mDegree + 1 );
253
254 // Initialize temp arrays with control points and weights
255 for ( int j = 0; j <= mDegree; ++j )
256 {
257 const int cpIdx = span - mDegree + j;
258 const QgsPoint &cp = mControlPoints[cpIdx];
259 const double w = ( cpIdx < mWeights.size() ) ? mWeights[cpIdx] : 1.0;
260
261 // Store in homogeneous coordinates (w * P)
262 tempX[j] = cp.x() * w;
263 tempY[j] = cp.y() * w;
264 tempZ[j] = hasZ ? cp.z() * w : 0.0;
265 tempM[j] = hasM ? cp.m() : 0.0; // M is not weighted
266 tempW[j] = w;
267 }
268
269 // De Boor iteration (Algorithm A4.1) in homogeneous space
270 for ( int k = 1; k <= mDegree; ++k )
271 {
272 for ( int j = mDegree; j >= k; --j )
273 {
274 const int knotIdx = span - mDegree + j;
275 const double denom = mKnots[knotIdx + mDegree - k + 1] - mKnots[knotIdx];
276
277 if ( !qgsDoubleNear( denom, 0.0 ) )
278 {
279 const double alpha = ( u - mKnots[knotIdx] ) / denom;
280 const double oneMinusAlpha = 1.0 - alpha;
281
282 // Linear interpolation in homogeneous space
283 tempX[j] = oneMinusAlpha * tempX[j - 1] + alpha * tempX[j];
284 tempY[j] = oneMinusAlpha * tempY[j - 1] + alpha * tempY[j];
285 if ( hasZ )
286 tempZ[j] = oneMinusAlpha * tempZ[j - 1] + alpha * tempZ[j];
287 if ( hasM )
288 tempM[j] = oneMinusAlpha * tempM[j - 1] + alpha * tempM[j];
289
290 // Interpolate weights
291 tempW[j] = oneMinusAlpha * tempW[j - 1] + alpha * tempW[j];
292 }
293 }
294 }
295
296 // Result is in temp[degree], stored in homogeneous coordinates
297 // Project back to Cartesian by dividing by weight
298 double x = tempX[mDegree];
299 double y = tempY[mDegree];
300 double z = tempZ[mDegree];
301 double m = tempM[mDegree];
302 const double w = tempW[mDegree];
303
304 if ( !qgsDoubleNear( w, 0.0 ) && !qgsDoubleNear( w, 1.0 ) )
305 {
306 x /= w;
307 y /= w;
308 if ( hasZ )
309 z /= w;
310 // M is not divided by weight (it's not in homogeneous space)
311 }
312
313 // Create point with appropriate dimensionality
314 if ( hasZ && hasM )
315 return QgsPoint( x, y, z, m );
316 else if ( hasZ )
317 return QgsPoint( x, y, z );
318 else if ( hasM )
319 return QgsPoint( x, y, std::numeric_limits<double>::quiet_NaN(), m );
320 else
321 return QgsPoint( x, y );
322}
323
325{
326 if ( mControlPoints.size() < 2 )
327 return false;
328
329 // Check if curve endpoints are the same by evaluating at t=0 and t=1
330 const QgsPoint startPt = evaluate( 0.0 );
331 const QgsPoint endPt = evaluate( 1.0 );
332
333 bool closed = qgsDoubleNear( startPt.x(), endPt.x() ) && qgsDoubleNear( startPt.y(), endPt.y() );
334
335 if ( is3D() && closed )
336 closed &= qgsDoubleNear( startPt.z(), endPt.z() ) || ( std::isnan( startPt.z() ) && std::isnan( endPt.z() ) );
337
338 return closed;
339}
340
342{
343 if ( mControlPoints.size() < 2 )
344 return false;
345
346 // Check if curve endpoints are the same in 2D
347 const QgsPoint startPt = evaluate( 0.0 );
348 const QgsPoint endPt = evaluate( 1.0 );
349
350 return qgsDoubleNear( startPt.x(), endPt.x() ) && qgsDoubleNear( startPt.y(), endPt.y() );
351}
352
354{
355 Q_UNUSED( toleranceType );
356
357 // Determine number of segments based on tolerance (angular approximation)
358 // For NURBS curves, we use uniform parameterization as a first approximation
359 const int steps = std::max( 2, static_cast<int>( 2 * M_PI / tolerance ) );
360
361 auto line = new QgsLineString();
362 for ( int i = 0; i <= steps; ++i )
363 {
364 const double t = static_cast<double>( i ) / steps;
365 const QgsPoint pt = evaluate( t );
366 line->addVertex( pt );
367 }
368
369 return line;
370}
371
372void QgsNurbsCurve::draw( QPainter &p ) const
373{
374 std::unique_ptr<QgsLineString> line( curveToLine() );
375 if ( line )
376 line->draw( p );
377}
378
379void QgsNurbsCurve::drawAsPolygon( QPainter &p ) const
380{
381 std::unique_ptr<QgsLineString> line( curveToLine() );
382 if ( line )
383 line->drawAsPolygon( p );
384}
385
387{
388 std::unique_ptr<QgsLineString> line( curveToLine() );
389 return line ? line->asQPolygonF() : QPolygonF();
390}
391
393{
394 return mControlPoints.isEmpty() ? QgsPoint() : mControlPoints.last();
395}
396
397bool QgsNurbsCurve::equals( const QgsCurve &other ) const
398{
399 if ( geometryType() != other.geometryType() )
400 {
401 return false;
402 }
403
405 if ( !o )
406 return false;
407
408 if ( o->mDegree != mDegree )
409 {
410 return false;
411 }
412
413 if ( mControlPoints != o->mControlPoints )
414 return false;
415
416 if ( mWeights != o->mWeights )
417 return false;
418
419 if ( mKnots != o->mKnots )
420 return false;
421
422 return true;
423}
424
425int QgsNurbsCurve::indexOf( const QgsPoint &point ) const
426{
427 for ( int i = 0; i < mControlPoints.size(); ++i )
428 {
429 if ( qgsDoubleNear( mControlPoints[i].distance( point ), 0.0 ) )
430 {
431 return i;
432 }
433 }
434 return -1;
435}
436
438{
439 std::unique_ptr<QgsLineString> line( curveToLine() );
440 if ( !line )
441 {
442 return nullptr;
443 }
444 return line->interpolatePoint( distance );
445}
446
448{
449 return mControlPoints.size();
450}
451
452bool QgsNurbsCurve::pointAt( int node, QgsPoint &point, Qgis::VertexType &type ) const
453{
454 if ( node < 0 || node >= mControlPoints.size() )
455 {
456 return false;
457 }
458 point = mControlPoints[node];
460 return true;
461}
462
464{
465 pts.reserve( pts.size() + mControlPoints.size() );
466 for ( const QgsPoint &p : mControlPoints )
467 {
468 pts.append( p );
469 }
470}
471
473{
474 auto rev = new QgsNurbsCurve( *this );
475 std::reverse( rev->mControlPoints.begin(), rev->mControlPoints.end() );
476 std::reverse( rev->mWeights.begin(), rev->mWeights.end() );
477
478 // Reverse and remap knot vector: new_knot[i] = max_knot + min_knot - old_knot[n-1-i]
479 if ( !rev->mKnots.isEmpty() )
480 {
481 const double maxKnot = rev->mKnots.last();
482 const double minKnot = rev->mKnots.first();
483 std::reverse( rev->mKnots.begin(), rev->mKnots.end() );
484 for ( double &knot : rev->mKnots )
485 {
486 knot = maxKnot + minKnot - knot;
487 }
488 }
489
490 return rev;
491}
492
493void QgsNurbsCurve::scroll( int firstVertexIndex )
494{
495 // Scrolling only makes sense for closed curves
496 if ( !isClosed() || firstVertexIndex <= 0 || firstVertexIndex >= mControlPoints.size() )
497 {
498 return;
499 }
500
501 // Rotate control points and weights
502 std::rotate( mControlPoints.begin(), mControlPoints.begin() + firstVertexIndex, mControlPoints.end() );
503 std::rotate( mWeights.begin(), mWeights.begin() + firstVertexIndex, mWeights.end() );
504
505 // Rotate knot vector and adjust values to preserve parameter domain
506 if ( !mKnots.isEmpty() && firstVertexIndex < mKnots.size() )
507 {
508 const double delta = mKnots[firstVertexIndex] - mKnots[0];
509 std::rotate( mKnots.begin(), mKnots.begin() + firstVertexIndex, mKnots.end() );
510 // Shift all knot values by -delta to preserve the start parameter
511 for ( double &knot : mKnots )
512 {
513 knot -= delta;
514 }
515 }
516
517 clearCache();
518}
519
520std::tuple<std::unique_ptr<QgsCurve>, std::unique_ptr<QgsCurve>>
522{
523 std::unique_ptr<QgsLineString> line( curveToLine() );
524 if ( !line )
525 {
526 return std::make_tuple( nullptr, nullptr );
527 }
528 return line->splitCurveAtVertex( index );
529}
530
532{
533 return mControlPoints.isEmpty() ? QgsPoint() : mControlPoints.first();
534}
535
536void QgsNurbsCurve::sumUpArea( double &sum ) const
537{
538 // TODO - investigate whether this can be calculated directly
539 std::unique_ptr<QgsLineString> line( curveToLine() );
540 if ( line )
541 line->sumUpArea( sum );
542}
543
544void QgsNurbsCurve::sumUpArea3D( double &sum ) const
545{
546 std::unique_ptr<QgsLineString> line( curveToLine() );
547 if ( line )
548 line->sumUpArea3D( sum );
549}
550
551double QgsNurbsCurve::xAt( int index ) const
552{
553 if ( index < 0 || index >= mControlPoints.size() )
554 return 0.0;
555 return mControlPoints[index].x();
556}
557
558double QgsNurbsCurve::yAt( int index ) const
559{
560 if ( index < 0 || index >= mControlPoints.size() )
561 return 0.0;
562 return mControlPoints[index].y();
563}
564
565double QgsNurbsCurve::zAt( int index ) const
566{
567 if ( index < 0 || index >= mControlPoints.size() )
568 return 0.0;
569 return mControlPoints[index].is3D() ? mControlPoints[index].z() : std::numeric_limits<double>::quiet_NaN();
570}
571
572double QgsNurbsCurve::mAt( int index ) const
573{
574 if ( index < 0 || index >= mControlPoints.size() )
575 return 0.0;
576 return mControlPoints[index].isMeasure() ? mControlPoints[index].m() : std::numeric_limits<double>::quiet_NaN();
577}
578
579bool QgsNurbsCurve::addZValue( double zValue )
580{
582 return false;
583
584 clearCache();
586
587 for ( QgsPoint &p : mControlPoints )
588 {
589 p.addZValue( zValue );
590 }
591
592 return true;
593}
594
595bool QgsNurbsCurve::addMValue( double mValue )
596{
598 return false;
599
600 clearCache();
602
603 for ( QgsPoint &p : mControlPoints )
604 {
605 p.addMValue( mValue );
606 }
607
608 return true;
609}
610
612{
613 if ( !is3D() )
614 return false;
615
616 for ( QgsPoint &p : mControlPoints )
617 {
618 p.setZ( std::numeric_limits<double>::quiet_NaN() );
619 }
620
622 clearCache();
623 return true;
624}
625
627{
628 if ( !isMeasure() )
629 return false;
630
631 for ( QgsPoint &p : mControlPoints )
632 {
633 p.setM( std::numeric_limits<double>::quiet_NaN() );
634 }
635
637 clearCache();
638 return true;
639}
640
642{
643 if ( position.part != 0 || position.ring != 0 )
644 {
645 return false;
646 }
647 const int idx = position.vertex;
648 if ( idx < 0 || idx >= mControlPoints.size() )
649 {
650 return false;
651 }
652 mControlPoints.remove( idx );
653 if ( idx < mWeights.size() )
654 {
655 mWeights.remove( idx );
656 }
657
659
660 clearCache();
661 return true;
662}
663
664void QgsNurbsCurve::filterVertices( const std::function<bool( const QgsPoint & )> &filter )
665{
666 QVector<QgsPoint> newPts;
667 QVector<double> newWeights;
668 for ( int i = 0; i < mControlPoints.size(); ++i )
669 {
670 if ( filter( mControlPoints[i] ) )
671 {
672 newPts.append( mControlPoints[i] );
673 if ( i < mWeights.size() )
674 newWeights.append( mWeights[i] );
675 }
676 }
677 mControlPoints = newPts;
678 mWeights = newWeights;
679
681
682 clearCache();
683}
684
685QVector<double> QgsNurbsCurve::generateUniformKnots( int numControlPoints, int degree )
686{
687 Q_ASSERT( numControlPoints > degree );
688
689 const int knotsSize = numControlPoints + degree + 1;
690 QVector<double> knots;
691 knots.reserve( knotsSize );
692 for ( int i = 0; i < knotsSize; ++i )
693 {
694 if ( i <= degree )
695 knots.append( 0.0 );
696 else if ( i >= numControlPoints )
697 knots.append( 1.0 );
698 else
699 knots.append( static_cast<double>( i - degree ) / ( numControlPoints - degree ) );
700 }
701 return knots;
702}
703
704QVector<double> QgsNurbsCurve::generateKnotsForBezierConversion( int nAnchors, int degree )
705{
706 if ( nAnchors < 2 || degree < 1 )
707 return QVector<double>();
708
709 const int segmentCount = nAnchors - 1;
710 const int totalKnots = degree * nAnchors + degree + 1;
711
712 QVector<double> knots;
713 knots.reserve( totalKnots );
714
715 // Clamping start: (degree + 1) knots at 0
716 for ( int i = 0; i < degree + 1; ++i )
717 knots.append( 0.0 );
718
719 // Interior: multiplicity 'degree' at each junction
720 for ( int segmentIndex = 1; segmentIndex < segmentCount; ++segmentIndex )
721 {
722 for ( int j = 0; j < degree; ++j )
723 knots.append( static_cast<double>( segmentIndex ) );
724 }
725
726 // Clamping end: (degree + 1) knots at segmentCount
727 for ( int i = 0; i < degree + 1; ++i )
728 knots.append( static_cast<double>( segmentCount ) );
729
730 return knots;
731}
732
734{
735 mKnots = generateUniformKnots( mControlPoints.size(), mDegree );
736}
737
739{
740 clear();
741
742 if ( !wkb )
743 return false;
744
745 // Store header endianness
746 const unsigned char headerEndianness = *static_cast<const unsigned char *>( wkb );
747
748 Qgis::WkbType type = wkb.readHeader();
749 if ( !QgsWkbTypes::isNurbsType( type ) )
750 return false;
751
752 mWkbType = type;
753 const bool is3D = QgsWkbTypes::hasZ( type );
754 const bool isMeasure = QgsWkbTypes::hasM( type );
755
756 // Read degree (4 bytes uint32)
757 quint32 degree;
758 wkb >> degree;
759
760 // Validate degree before casting to int
761 if ( degree < 1 || degree > static_cast<quint32>( std::numeric_limits<int>::max() ) )
762 return false;
763
764 mDegree = static_cast<int>( degree );
765
766 // Read number of control points (4 bytes uint32)
767 quint32 numControlPoints;
768 wkb >> numControlPoints;
769
770 // Sanity check: numControlPoints should be reasonable given the WKB blob size
771 // Each control point needs at least:
772 // - 1 byte (endianness)
773 // - 16 bytes (x,y)
774 // - 8 bytes (z) if 3D
775 // - 8 bytes (m) if measure
776 // - 1 byte (weight flag)
777 // Minimum: 18 bytes (2D) to 34 bytes (ZM)
778 const int minBytesPerPoint = 18 + ( is3D ? 8 : 0 ) + ( isMeasure ? 8 : 0 );
779 if ( numControlPoints > static_cast<quint32>( wkb.remaining() / minBytesPerPoint + 1 ) )
780 return false;
781
782 mControlPoints.clear();
783 mWeights.clear();
784 mControlPoints.reserve( numControlPoints );
785 mWeights.reserve( numControlPoints );
786
787 // Read control points
788 for ( quint32 i = 0; i < numControlPoints; ++i )
789 {
790 // Read byte order for this point (1 byte)
791 char pointEndianness;
792 wkb >> pointEndianness;
793
794 // Validate endianness: must be 0 (big-endian) or 1 (little-endian)
795 // and must match the WKB header endianness
796 if ( static_cast<unsigned char>( pointEndianness ) != headerEndianness )
797 return false;
798
799 // Read coordinates
800 double x, y, z = 0.0, m = 0.0;
801 wkb >> x >> y;
802
803 if ( is3D )
804 wkb >> z;
805 if ( isMeasure )
806 wkb >> m;
807
808 // Read weight flag (1 byte)
809 char weightFlag;
810 wkb >> weightFlag;
811
812 double weight = 1.0;
813 if ( weightFlag == 1 )
814 {
815 // Read custom weight (8 bytes double)
816 wkb >> weight;
817 }
818
819 // Create point with appropriate dimensionality
820 QgsPoint point;
821 if ( is3D && isMeasure )
822 point = QgsPoint( x, y, z, m );
823 else if ( is3D )
824 point = QgsPoint( x, y, z );
825 else if ( isMeasure )
826 point = QgsPoint( x, y, std::numeric_limits<double>::quiet_NaN(), m );
827 else
828 point = QgsPoint( x, y );
829
830 mControlPoints.append( point );
831 mWeights.append( weight );
832 }
833
834 // Read number of knots (4 bytes uint32)
835 quint32 numKnots;
836 wkb >> numKnots;
837
838 // Sanity check: numKnots should be numControlPoints + degree + 1
839 const quint32 expectedKnots = numControlPoints + degree + 1;
840 if ( numKnots != expectedKnots )
841 return false;
842
843 // Sanity check: remaining WKB should have enough bytes for knots
844 if ( numKnots * sizeof( double ) > static_cast<quint32>( wkb.remaining() ) )
845 return false;
846
847 mKnots.clear();
848 mKnots.reserve( numKnots );
849
850 // Read knot values (8 bytes double each)
851 for ( quint32 i = 0; i < numKnots; ++i )
852 {
853 double knot;
854 wkb >> knot;
855 mKnots.append( knot );
856 }
857
858 return true;
859}
860
861bool QgsNurbsCurve::fromWkt( const QString &wkt )
862{
863 clear();
864
865 const QString geomTypeStr = wkt.split( '(' )[0].trimmed().toUpper();
866
867 if ( !geomTypeStr.startsWith( "NURBSCURVE"_L1 ) )
868 {
869 return false;
870 }
871
872 // Determine dimensionality from the geometry type string
873 // Handle both "NURBSCURVEZM" and "NURBSCURVE ZM" formats
874 if ( geomTypeStr.contains( "ZM"_L1 ) )
876 else if ( geomTypeStr.endsWith( 'Z' ) || geomTypeStr.endsWith( " Z"_L1 ) )
878 else if ( geomTypeStr.endsWith( 'M' ) || geomTypeStr.endsWith( " M"_L1 ) )
880 else
882
883 QPair<Qgis::WkbType, QString> parts = QgsGeometryUtils::wktReadBlock( wkt );
884
885 if ( parts.second.compare( "EMPTY"_L1, Qt::CaseInsensitive ) == 0 || parts.second.isEmpty() )
886 return true;
887
888 // Split the content by commas at parentheses level 0
889 QStringList blocks = QgsGeometryUtils::wktGetChildBlocks( parts.second, QString() );
890
891 if ( blocks.isEmpty() )
892 return false;
893
894 // First block should be the degree
895 bool ok = true;
896 int degree = blocks[0].trimmed().toInt( &ok );
897 if ( !ok || degree < 1 )
898 return false;
899
900 if ( blocks.size() < 2 )
901 return false;
902
903 // Second block should be the control points
904 QString pointsStr = blocks[1].trimmed();
905
906 // Validate control points block starts with '(' and ends with ')'
907 if ( !pointsStr.startsWith( '('_L1 ) || !pointsStr.endsWith( ')'_L1 ) )
908 return false;
909
910 pointsStr = pointsStr.mid( 1, pointsStr.length() - 2 ).trimmed();
911
912 // Parse control points
913 QStringList pointsCoords = pointsStr.split( ',', Qt::SkipEmptyParts );
914 QVector<QgsPoint> controlPoints;
915
916 const thread_local QRegularExpression rx( u"\\s+"_s );
917
918 for ( const QString &pointStr : pointsCoords )
919 {
920 QStringList coords = pointStr.trimmed().split( rx, Qt::SkipEmptyParts );
921
922 if ( coords.size() < 2 )
923 return false;
924
925 QgsPoint point;
926 bool ok = true;
927
928 double x = coords[0].toDouble( &ok );
929 if ( !ok )
930 return false;
931
932 double y = coords[1].toDouble( &ok );
933 if ( !ok )
934 return false;
935
936 // Handle different coordinate patterns based on declared geometry type
937 if ( coords.size() >= 3 )
938 {
939 if ( isMeasure() && !is3D() && coords.size() == 3 )
940 {
941 // NURBSCURVE M pattern: (x y m) - third coordinate is M, not Z
942 double m = coords[2].toDouble( &ok );
943 if ( !ok )
944 return false;
945 point = QgsPoint( x, y, std::numeric_limits<double>::quiet_NaN(), m );
946 }
947 else if ( is3D() && !isMeasure() && coords.size() >= 3 )
948 {
949 // NURBSCURVE Z pattern: (x y z)
950 double z = coords[2].toDouble( &ok );
951 if ( !ok )
952 return false;
953 point = QgsPoint( x, y, z );
954 }
955 else if ( is3D() && isMeasure() && coords.size() >= 4 )
956 {
957 // NURBSCURVE ZM pattern: (x y z m)
958 double z = coords[2].toDouble( &ok );
959 if ( !ok )
960 return false;
961 double m = coords[3].toDouble( &ok );
962 if ( !ok )
963 return false;
964 point = QgsPoint( x, y, z, m );
965 }
966 else if ( isMeasure() && coords.size() >= 4 )
967 {
968 // NURBSCURVE M pattern with 4 coords: (x y z m) - upgrade to ZM
969 double z = coords[2].toDouble( &ok );
970 if ( !ok )
971 return false;
972 double m = coords[3].toDouble( &ok );
973 if ( !ok )
974 return false;
975 point = QgsPoint( x, y, z, m );
976 if ( !is3D() )
978 }
979 else if ( !is3D() && !isMeasure() && coords.size() == 3 )
980 {
981 // No explicit dimension - auto-upgrade to 3D: (x y z)
982 double z = coords[2].toDouble( &ok );
983 if ( !ok )
984 return false;
985 point = QgsPoint( x, y, z );
987 }
988 else if ( !is3D() && !isMeasure() && coords.size() >= 4 )
989 {
990 // No explicit dimension - auto-upgrade to ZM: (x y z m)
991 double z = coords[2].toDouble( &ok );
992 if ( !ok )
993 return false;
994 double m = coords[3].toDouble( &ok );
995 if ( !ok )
996 return false;
997 point = QgsPoint( x, y, z, m );
1000 }
1001 else
1002 {
1003 // Only 2 coordinates but declared type may require Z or M
1004 if ( is3D() && isMeasure() )
1005 point = QgsPoint( Qgis::WkbType::PointZM, x, y, 0.0, 0.0 );
1006 else if ( is3D() )
1007 point = QgsPoint( Qgis::WkbType::PointZ, x, y, 0.0 );
1008 else if ( isMeasure() )
1009 point = QgsPoint( Qgis::WkbType::PointM, x, y, std::numeric_limits<double>::quiet_NaN(), 0.0 );
1010 else
1011 point = QgsPoint( x, y );
1012 }
1013 }
1014 else
1015 {
1016 // Only 2 coordinates - create point matching declared type
1017 if ( is3D() && isMeasure() )
1018 point = QgsPoint( Qgis::WkbType::PointZM, x, y, 0.0, 0.0 );
1019 else if ( is3D() )
1020 point = QgsPoint( Qgis::WkbType::PointZ, x, y, 0.0 );
1021 else if ( isMeasure() )
1022 point = QgsPoint( Qgis::WkbType::PointM, x, y, std::numeric_limits<double>::quiet_NaN(), 0.0 );
1023 else
1024 point = QgsPoint( x, y );
1025 }
1026
1027 controlPoints.append( point );
1028 }
1029
1030 mControlPoints = controlPoints;
1031
1032 // Initialize weights to 1.0 (non-rational by default)
1033 mWeights.clear();
1034 for ( int i = 0; i < controlPoints.size(); ++i )
1035 {
1036 mWeights.append( 1.0 );
1037 }
1038
1039 // Parse additional parameters (degree already parsed at the beginning)
1040 bool hasWeights = false;
1041 bool hasKnots = false;
1042
1043 // Process remaining blocks (starting from index 2 since 0=degree, 1=control points)
1044 for ( int i = 2; i < blocks.size(); ++i )
1045 {
1046 QString block = blocks[i].trimmed();
1047
1048 if ( block.startsWith( '('_L1 ) )
1049 {
1050 // Validate block ends with ')'
1051 if ( !block.endsWith( ')'_L1 ) )
1052 return false;
1053
1054 // This could be weights or knots vector
1055 block = block.mid( 1, block.length() - 2 ).trimmed();
1056 QStringList values = block.split( ',', Qt::SkipEmptyParts );
1057
1058 QVector<double> parsedValues;
1059 for ( const QString &valueStr : values )
1060 {
1061 bool ok = true;
1062 double value = valueStr.trimmed().toDouble( &ok );
1063 if ( !ok )
1064 return false;
1065 parsedValues.append( value );
1066 }
1067
1068 if ( !hasWeights && parsedValues.size() == controlPoints.size() )
1069 {
1070 // This is the weights vector
1071 mWeights = parsedValues;
1072 hasWeights = true;
1073 }
1074 else if ( !hasKnots )
1075 {
1076 // This is the knots vector
1077 mKnots = parsedValues;
1078 hasKnots = true;
1079 }
1080 }
1081 else
1082 {
1083 // Invalid block - doesn't start with '('
1084 return false;
1085 }
1086 }
1087
1088 mDegree = degree;
1089
1090 // Validate: need at least (degree + 1) control points
1091 if ( controlPoints.size() <= degree )
1092 return false;
1093
1094 // If no knots were provided, create default knots (open uniform)
1095 if ( !hasKnots )
1096 {
1098 }
1099
1100 return true;
1101}
1102
1103bool QgsNurbsCurve::fuzzyEqual( const QgsAbstractGeometry &other, double epsilon ) const
1104{
1106 if ( !o )
1107 return false;
1108
1109 if ( mDegree != o->mDegree || mControlPoints.size() != o->mControlPoints.size() || mWeights.size() != o->mWeights.size() || mKnots.size() != o->mKnots.size() )
1110 {
1111 return false;
1112 }
1113
1114 for ( int i = 0; i < mControlPoints.size(); ++i )
1115 {
1116 if ( mControlPoints[i].distance( o->mControlPoints[i] ) >= epsilon )
1117 return false;
1118 }
1119
1120 for ( int i = 0; i < mWeights.size(); ++i )
1121 {
1122 if ( std::fabs( mWeights[i] - o->mWeights[i] ) > epsilon )
1123 return false;
1124 }
1125
1126 for ( int i = 0; i < mKnots.size(); ++i )
1127 {
1128 if ( std::fabs( mKnots[i] - o->mKnots[i] ) > epsilon )
1129 return false;
1130 }
1131
1132 return true;
1133}
1134
1135bool QgsNurbsCurve::fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon ) const
1136{
1137 return fuzzyEqual( other, epsilon );
1138}
1139
1141{
1142 return u"NurbsCurve"_s;
1143}
1144
1146{
1147 return true;
1148}
1149
1151{
1152 return 1;
1153}
1154
1156{
1157 return new QgsNurbsCurve( *this );
1158}
1159
1161{
1162 if ( id.part != 0 || id.ring != 0 )
1163 {
1164 return QgsPoint();
1165 }
1166 const int idx = id.vertex;
1167 if ( idx < 0 || idx >= mControlPoints.size() )
1168 {
1169 return QgsPoint();
1170 }
1171 return mControlPoints[idx];
1172}
1173
1174int QgsNurbsCurve::vertexCount( int part, int ring ) const
1175{
1176 return ( part == 0 && ring == 0 ) ? mControlPoints.size() : 0;
1177}
1178
1180{
1181 if ( id.part == 0 && id.ring == 0 )
1182 {
1183 return id.vertex;
1184 }
1185 return -1;
1186}
1187
1188bool QgsNurbsCurve::isValid( QString &error, Qgis::GeometryValidityFlags flags ) const
1189{
1190 Q_UNUSED( flags );
1191
1192 // Use cached validity if available
1193 if ( mValidityComputed )
1194 {
1195 if ( !mIsValid )
1196 error = u"NURBS curve is invalid"_s;
1197 return mIsValid;
1198 }
1199
1200 mValidityComputed = true;
1201 mIsValid = false;
1202
1203 if ( mDegree < 1 )
1204 {
1205 error = u"Degree must be >= 1"_s;
1206 return false;
1207 }
1208
1209 const int n = mControlPoints.size();
1210 if ( n < mDegree + 1 )
1211 {
1212 error = u"Not enough control points for degree"_s;
1213 return false;
1214 }
1215
1216 if ( mKnots.size() != n + mDegree + 1 )
1217 {
1218 error = u"Knot vector size is incorrect"_s;
1219 return false;
1220 }
1221
1222 if ( mWeights.size() != n )
1223 {
1224 error = u"Weights vector size mismatch"_s;
1225 return false;
1226 }
1227
1228 // Check that knots are non-decreasing
1229 for ( int i = 1; i < mKnots.size(); ++i )
1230 {
1231 if ( mKnots[i] < mKnots[i - 1] )
1232 {
1233 error = u"Knot vector values must be non-decreasing"_s;
1234 return false;
1235 }
1236 }
1237
1238 mIsValid = true;
1239 return true;
1240}
1241
1242void QgsNurbsCurve::addToPainterPath( QPainterPath &path ) const
1243{
1244 std::unique_ptr<QgsLineString> line( curveToLine() );
1245 if ( line )
1246 line->addToPainterPath( path );
1247}
1248
1249QgsCurve *QgsNurbsCurve::curveSubstring( double startDistance, double endDistance ) const
1250{
1251 std::unique_ptr<QgsLineString> line( curveToLine() );
1252 if ( !line )
1253 return nullptr;
1254 return line->curveSubstring( startDistance, endDistance );
1255}
1256
1258{
1259 std::unique_ptr<QgsLineString> line( curveToLine() );
1260 return line ? line->length() : 0.0;
1261}
1262
1264{
1265 std::unique_ptr<QgsLineString> line( curveToLine() );
1266 if ( !line )
1267 return 0.0;
1268 return line->segmentLength( startVertex );
1269}
1270
1272{
1273 std::unique_ptr<QgsLineString> line( curveToLine() );
1274 if ( !line )
1275 return -1.0;
1276 return line->distanceBetweenVertices( fromVertex, toVertex );
1277}
1278
1279QgsAbstractGeometry *QgsNurbsCurve::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const
1280{
1281 auto result = new QgsNurbsCurve( *this );
1282 for ( QgsPoint &pt : result->mControlPoints )
1283 {
1284 if ( hSpacing > 0 )
1285 pt.setX( std::round( pt.x() / hSpacing ) * hSpacing );
1286 if ( vSpacing > 0 )
1287 pt.setY( std::round( pt.y() / vSpacing ) * vSpacing );
1288 if ( pt.is3D() && dSpacing > 0 )
1289 pt.setZ( std::round( pt.z() / dSpacing ) * dSpacing );
1290 if ( pt.isMeasure() && mSpacing > 0 )
1291 pt.setM( std::round( pt.m() / mSpacing ) * mSpacing );
1292 }
1293
1294 if ( removeRedundantPoints )
1295 result->removeDuplicateNodes();
1296
1297 return result;
1298}
1299
1301{
1302 std::unique_ptr<QgsLineString> line( curveToLine() );
1303 if ( !line )
1304 return new QgsNurbsCurve( *this );
1305 return line->simplifyByDistance( tolerance );
1306}
1307
1308bool QgsNurbsCurve::removeDuplicateNodes( double epsilon, bool useZValues )
1309{
1310 if ( mControlPoints.size() < 2 )
1311 return false;
1312
1313 QVector<QgsPoint> newPoints;
1314 QVector<double> newWeights;
1315
1316 newPoints.reserve( mControlPoints.size() );
1317 newWeights.reserve( mWeights.size() );
1318
1319 newPoints.append( mControlPoints.first() );
1320 if ( !mWeights.isEmpty() )
1321 newWeights.append( mWeights.first() );
1322
1323 for ( int i = 1; i < mControlPoints.size(); ++i )
1324 {
1325 const double dist = ( useZValues && mControlPoints[i].is3D() && mControlPoints[i - 1].is3D() )
1326 ? mControlPoints[i].distance3D( mControlPoints[i - 1] )
1327 : mControlPoints[i].distance( mControlPoints[i - 1] );
1328
1329 if ( dist >= epsilon )
1330 {
1331 newPoints.append( mControlPoints[i] );
1332 if ( i < mWeights.size() )
1333 newWeights.append( mWeights[i] );
1334 }
1335 }
1336
1337 const bool changed = ( newPoints.size() != mControlPoints.size() );
1338 if ( !changed )
1339 return false;
1340
1341 mControlPoints = newPoints;
1342 mWeights = newWeights;
1343
1344 // Regenerate uniform knot vector for the new number of control points
1346
1347 clearCache();
1348 return true;
1349}
1350
1352{
1353 std::unique_ptr<QgsLineString> line( curveToLine() );
1354 if ( !line )
1355 return 0.0;
1356 return line->vertexAngle( vertex );
1357}
1358
1360{
1361 for ( QgsPoint &pt : mControlPoints )
1362 {
1363 const double x = pt.x();
1364 pt.setX( pt.y() );
1365 pt.setY( x );
1366 }
1367 clearCache();
1368}
1369
1371{
1372 Q_UNUSED( feedback );
1373 if ( !transformer )
1374 return false;
1375
1376 for ( QgsPoint &pt : mControlPoints )
1377 {
1378 double x = pt.x(), y = pt.y(), z = pt.z(), m = pt.m();
1379 if ( !transformer->transformPoint( x, y, z, m ) )
1380 return false;
1381 pt.setX( x );
1382 pt.setY( y );
1383 pt.setZ( z );
1384 pt.setM( m );
1385 }
1386
1387 clearCache();
1388 return true;
1389}
1390
1395
1396double QgsNurbsCurve::closestSegment( const QgsPoint &pt, QgsPoint &segmentPt, QgsVertexId &vertexAfter, int *leftOf, double epsilon ) const
1397{
1398 std::unique_ptr<QgsLineString> line( curveToLine() );
1399 if ( !line )
1400 {
1401 segmentPt = QgsPoint();
1402 vertexAfter = QgsVertexId();
1403 if ( leftOf )
1404 *leftOf = 0;
1405 return -1;
1406 }
1407 return line->closestSegment( pt, segmentPt, vertexAfter, leftOf, epsilon );
1408}
1409
1411{
1412 for ( QgsPoint &pt : mControlPoints )
1413 {
1414 double x = pt.x();
1415 double y = pt.y();
1416 double z = transformZ && pt.is3D() ? pt.z() : std::numeric_limits<double>::quiet_NaN();
1417 ct.transformInPlace( x, y, z, d );
1418 pt.setX( x );
1419 pt.setY( y );
1420 if ( transformZ && pt.is3D() )
1421 pt.setZ( z );
1422 }
1423 clearCache();
1424}
1425
1426void QgsNurbsCurve::transform( const QTransform &t, double zTranslate, double zScale, double mTranslate, double mScale )
1427{
1428 for ( QgsPoint &pt : mControlPoints )
1429 {
1430 const QPointF p = t.map( QPointF( pt.x(), pt.y() ) );
1431 pt.setX( p.x() );
1432 pt.setY( p.y() );
1433
1434 if ( pt.is3D() )
1435 pt.setZ( pt.z() * zScale + zTranslate );
1436 if ( pt.isMeasure() )
1437 pt.setM( pt.m() * mScale + mTranslate );
1438 }
1439 clearCache();
1440}
1441
1446
1448{
1449 if ( mBoundingBox.isNull() )
1450 {
1452 }
1453 return mBoundingBox;
1454}
1455
1457{
1458 if ( mControlPoints.isEmpty() )
1459 return QgsBox3D();
1460
1461 // The bounding box must include all control points, not just points on the curve.
1462 // This is important for Snapping to control points (they can lie outside the curve itself)
1463 QgsBox3D bbox;
1464 for ( const QgsPoint &pt : mControlPoints )
1465 {
1466 bbox.combineWith( pt.x(), pt.y(), pt.is3D() ? pt.z() : std::numeric_limits<double>::quiet_NaN() );
1467 }
1468
1469 // Also include points on the curve to ensure the bbox is complete
1470 std::unique_ptr<QgsLineString> line( curveToLine() );
1471 if ( line )
1472 {
1473 bbox.combineWith( line->boundingBox3D() );
1474 }
1475
1476 return bbox;
1477}
1478
1480{
1482 mValidityComputed = false;
1483 mIsValid = false;
1484}
1485
1486bool QgsNurbsCurve::moveVertex( QgsVertexId position, const QgsPoint &newPos )
1487{
1488 if ( position.part != 0 || position.ring != 0 )
1489 return false;
1490
1491 const int idx = position.vertex;
1492 if ( idx < 0 || idx >= mControlPoints.size() )
1493 return false;
1494
1495 mControlPoints[idx] = newPos;
1496 clearCache();
1497 return true;
1498}
1499
1500bool QgsNurbsCurve::insertVertex( QgsVertexId position, const QgsPoint &vertex )
1501{
1502 if ( position.part != 0 || position.ring != 0 )
1503 return false;
1504
1505 const int idx = position.vertex;
1506 if ( idx < 0 || idx > mControlPoints.size() )
1507 return false;
1508
1509 mControlPoints.insert( idx, vertex );
1510 if ( idx <= mWeights.size() )
1511 mWeights.insert( idx, 1.0 );
1512
1514
1515 clearCache();
1516 return true;
1517}
1518
1520{
1521 Q_UNUSED( flags );
1522
1523 const bool is3D = QgsWkbTypes::hasZ( mWkbType );
1524 const bool isMeasure = QgsWkbTypes::hasM( mWkbType );
1525 const int coordinateDimension = 2 + ( is3D ? 1 : 0 ) + ( isMeasure ? 1 : 0 );
1526
1527 int size = 0;
1528
1529 // WKB header (endianness + type)
1530 size += 1 + 4;
1531
1532 // Degree (4 bytes)
1533 size += 4;
1534
1535 // Number of control points (4 bytes)
1536 size += 4;
1537
1538 // Control points data
1539 for ( int i = 0; i < mControlPoints.size(); ++i )
1540 {
1541 // Point byte order (1 byte)
1542 size += 1;
1543
1544 // Coordinates (8 bytes per coordinate)
1545 size += coordinateDimension * 8;
1546
1547 // Weight flag (1 byte)
1548 size += 1;
1549
1550 // Weight value if not default (8 bytes)
1551 if ( i < mWeights.size() && std::fabs( mWeights[i] - 1.0 ) > 1e-10 )
1552 size += 8;
1553 }
1554
1555 // Number of knots (4 bytes)
1556 size += 4;
1557
1558 // Knot values (8 bytes each)
1559 size += mKnots.size() * 8;
1560
1561 return size;
1562}
1563
1565{
1566 QByteArray wkbArray;
1567 wkbArray.resize( QgsNurbsCurve::wkbSize( flags ) );
1568 QgsWkbPtr wkbPtr( wkbArray );
1569
1570 // Write WKB header
1571 wkbPtr << static_cast<char>( QgsApplication::endian() );
1572 wkbPtr << static_cast<quint32>( mWkbType );
1573
1574 // Write degree (4 bytes uint32)
1575 wkbPtr << static_cast<quint32>( mDegree );
1576
1577 // Write number of control points (4 bytes uint32)
1578 wkbPtr << static_cast<quint32>( mControlPoints.size() );
1579
1580 const bool is3D = QgsWkbTypes::hasZ( mWkbType );
1581 const bool isMeasure = QgsWkbTypes::hasM( mWkbType );
1582
1583 // Write control points
1584 for ( int i = 0; i < mControlPoints.size(); ++i )
1585 {
1586 const QgsPoint &point = mControlPoints[i];
1587
1588 // Write byte order for this point (1 byte) - use same as global
1589 wkbPtr << static_cast<char>( QgsApplication::endian() );
1590
1591 // Write coordinates
1592 wkbPtr << point.x() << point.y();
1593
1594 if ( is3D )
1595 wkbPtr << point.z();
1596 if ( isMeasure )
1597 wkbPtr << point.m();
1598
1599 // Write weight flag and weight
1600 const double weight = ( i < mWeights.size() ) ? mWeights[i] : 1.0;
1601 const bool hasCustomWeight = std::fabs( weight - 1.0 ) > 1e-10;
1602
1603 wkbPtr << static_cast<char>( hasCustomWeight ? 1 : 0 );
1604
1605 if ( hasCustomWeight )
1606 {
1607 wkbPtr << weight;
1608 }
1609 }
1610
1611 // Write number of knots (4 bytes uint32)
1612 wkbPtr << static_cast<quint32>( mKnots.size() );
1613
1614 // Write knot values (8 bytes double each)
1615 for ( const double knot : mKnots )
1616 {
1617 wkbPtr << knot;
1618 }
1619
1620 return wkbArray;
1621}
1622
1623QString QgsNurbsCurve::asWkt( int precision ) const
1624{
1625 QString wkt = wktTypeStr();
1626
1627 if ( isEmpty() )
1628 {
1629 wkt += " EMPTY"_L1;
1630 }
1631 else
1632 {
1633 wkt += " ("_L1;
1634
1635 // Add degree first
1636 wkt += QString::number( mDegree );
1637
1638 // Add control points
1639 wkt += ", ("_L1;
1640 for ( int i = 0; i < mControlPoints.size(); ++i )
1641 {
1642 if ( i > 0 )
1643 wkt += ", "_L1;
1644
1645 const QgsPoint &pt = mControlPoints[i];
1646 wkt += qgsDoubleToString( pt.x(), precision ) + ' ' + qgsDoubleToString( pt.y(), precision );
1647
1648 if ( pt.is3D() )
1649 wkt += ' ' + qgsDoubleToString( pt.z(), precision );
1650
1651 if ( pt.isMeasure() )
1652 wkt += ' ' + qgsDoubleToString( pt.m(), precision );
1653 }
1654 wkt += ')';
1655
1656 // Always add weights if they exist to ensure round-trip consistency
1657 if ( !mWeights.isEmpty() )
1658 {
1659 wkt += ", ("_L1;
1660 for ( int i = 0; i < mWeights.size(); ++i )
1661 {
1662 if ( i > 0 )
1663 wkt += ", "_L1;
1664 wkt += qgsDoubleToString( mWeights[i], precision );
1665 }
1666 wkt += ')';
1667 }
1668
1669 // Always add knots if they exist to ensure round-trip consistency
1670 if ( !mKnots.isEmpty() )
1671 {
1672 wkt += ", ("_L1;
1673 for ( int i = 0; i < mKnots.size(); ++i )
1674 {
1675 if ( i > 0 )
1676 wkt += ", "_L1;
1677 wkt += qgsDoubleToString( mKnots[i], precision );
1678 }
1679 wkt += ')';
1680 }
1681
1682 wkt += ')';
1683 }
1684
1685 return wkt;
1686}
1687
1688QDomElement QgsNurbsCurve::asGml2( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const
1689{
1690 // GML2 does not support NURBS curves, convert to LineString
1691 // TODO: GML3 has BSpline support, but it's not clear how it's handled elsewhere in QGIS
1692 std::unique_ptr<QgsLineString> line( curveToLine() );
1693 if ( !line )
1694 return QDomElement();
1695 return line->asGml2( doc, precision, ns, axisOrder );
1696}
1697
1698QDomElement QgsNurbsCurve::asGml3( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const
1699{
1700 // TODO: GML3 has native BSpline support (gml:BSpline), but it's not clear how it's handled elsewhere in QGIS
1701 // For now, convert to LineString for compatibility
1702 std::unique_ptr<QgsLineString> line( curveToLine() );
1703 if ( !line )
1704 return QDomElement();
1705 return line->asGml3( doc, precision, ns, axisOrder );
1706}
1707
1708json QgsNurbsCurve::asJsonObject( int precision ) const
1709{
1710 std::unique_ptr<QgsLineString> line( curveToLine() );
1711 if ( !line )
1712 return json::object();
1713 return line->asJsonObject( precision );
1714}
1715
1716QString QgsNurbsCurve::asKml( int precision ) const
1717{
1718 // KML does not support NURBS curves, convert to LineString
1719 std::unique_ptr<QgsLineString> line( curveToLine() );
1720 if ( !line )
1721 return QString();
1722 return line->asKml( precision );
1723}
1724
1726{
1727 return 1;
1728}
1729
1731{
1732 return mControlPoints.isEmpty();
1733}
1734
1736{
1737 mControlPoints.clear();
1738 mKnots.clear();
1739 mWeights.clear();
1740 mDegree = 0;
1741 clearCache();
1742}
1743
1745{
1746 return boundingBox().intersects( rectangle );
1747}
1748
1750{
1751 return boundingBox3D().intersects( box3d );
1752}
1753
1755{
1756 std::unique_ptr<QgsLineString> line( curveToLine() );
1757 return line ? line->centroid() : QgsPoint();
1758}
1759
1761{
1762 const QgsNurbsCurve *otherCurve = qgsgeometry_cast<const QgsNurbsCurve *>( other );
1763 if ( !otherCurve )
1764 return -1;
1765
1766 if ( mDegree < otherCurve->mDegree )
1767 return -1;
1768 else if ( mDegree > otherCurve->mDegree )
1769 return 1;
1770
1771 const int nThis = mControlPoints.size();
1772 const int nOther = otherCurve->mControlPoints.size();
1773
1774 if ( nThis < nOther )
1775 return -1;
1776 else if ( nThis > nOther )
1777 return 1;
1778
1779 for ( int i = 0; i < nThis; ++i )
1780 {
1781 if ( mControlPoints[i].x() < otherCurve->mControlPoints[i].x() )
1782 return -1;
1783 if ( mControlPoints[i].x() > otherCurve->mControlPoints[i].x() )
1784 return 1;
1785 if ( mControlPoints[i].y() < otherCurve->mControlPoints[i].y() )
1786 return -1;
1787 if ( mControlPoints[i].y() > otherCurve->mControlPoints[i].y() )
1788 return 1;
1789 }
1790
1791 if ( mWeights.size() < otherCurve->mWeights.size() )
1792 return -1;
1793 else if ( mWeights.size() > otherCurve->mWeights.size() )
1794 return 1;
1795
1796 for ( int i = 0; i < mWeights.size(); ++i )
1797 {
1798 if ( mWeights[i] < otherCurve->mWeights[i] )
1799 return -1;
1800 else if ( mWeights[i] > otherCurve->mWeights[i] )
1801 return 1;
1802 }
1803
1804 if ( mKnots.size() < otherCurve->mKnots.size() )
1805 return -1;
1806 else if ( mKnots.size() > otherCurve->mKnots.size() )
1807 return 1;
1808
1809 for ( int i = 0; i < mKnots.size(); ++i )
1810 {
1811 if ( mKnots[i] < otherCurve->mKnots[i] )
1812 return -1;
1813 else if ( mKnots[i] > otherCurve->mKnots[i] )
1814 return 1;
1815 }
1816
1817 return 0;
1818}
1819
1820double QgsNurbsCurve::weight( int index ) const
1821{
1822 if ( index < 0 || index >= mWeights.size() )
1823 return 1.0;
1824 return mWeights[index];
1825}
1826
1827bool QgsNurbsCurve::setWeight( int index, double weight )
1828{
1829 if ( index < 0 || index >= mWeights.size() )
1830 return false;
1831 if ( weight <= 0.0 )
1832 return false;
1833 mWeights[index] = weight;
1834 clearCache();
1835 return true;
1836}
1837
1838bool QgsNurbsCurve::isAnchorVertex( int localIndex ) const
1839{
1840 if ( !isPolyBezier() || localIndex < 0 || localIndex >= mControlPoints.size() )
1841 return false;
1842
1843 return ( localIndex % mDegree ) == 0;
1844}
QFlags< GeometryValidityFlag > GeometryValidityFlags
Geometry validity flags.
Definition qgis.h:2148
VertexType
Types of vertex.
Definition qgis.h:3151
@ ControlPoint
A NURBS control point (does not lie on the curve).
Definition qgis.h:3154
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:291
@ NurbsCurveM
NurbsCurveM.
Definition qgis.h:341
@ NurbsCurve
NurbsCurve.
Definition qgis.h:308
@ NurbsCurveZ
NurbsCurveZ.
Definition qgis.h:325
@ PointM
PointM.
Definition qgis.h:326
@ NurbsCurveZM
NurbsCurveZM.
Definition qgis.h:357
@ PointZ
PointZ.
Definition qgis.h:310
@ PointZM
PointZM.
Definition qgis.h:342
TransformDirection
Indicates the direction (forward or inverse) of a transform.
Definition qgis.h:2744
An abstract base class for classes which transform geometries by transforming input points to output ...
virtual bool transformPoint(double &x, double &y, double &z, double &m)=0
Transforms the point defined by the coordinates (x, y, z) and the specified m value.
SegmentationToleranceType
Segmentation tolerance as maximum angle or maximum difference between approximation and circle.
bool isMeasure() const
Returns true if the geometry contains m values.
QFlags< WkbFlag > WkbFlags
bool is3D() const
Returns true if the geometry is 3D and contains a z-value.
AxisOrder
Axis order for GML generation.
virtual QString geometryType() const =0
Returns a unique string representing the geometry type.
QString wktTypeStr() const
Returns the WKT type string of the geometry.
QgsAbstractGeometry()=default
QgsGeometryConstPartIterator parts() const
Returns Java-style iterator for traversal of parts of the geometry.
static endian_t endian()
Returns whether this machine uses big or little endian.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
bool intersects(const QgsBox3D &other) const
Returns true if box intersects with another box.
Definition qgsbox3d.cpp:150
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:381
void combineWith(const QgsBox3D &box)
Expands the bbox so that it covers both the original rectangle and the given rectangle.
Definition qgsbox3d.cpp:214
A const WKB pointer.
Definition qgswkbptr.h:139
int remaining() const
remaining
Definition qgswkbptr.h:196
Qgis::WkbType readHeader() const
readHeader
Definition qgswkbptr.cpp:60
Handles coordinate transforms between two coordinate systems.
void transformInPlace(double &x, double &y, double &z, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transforms an array of x, y and z double coordinates in place, from the source CRS to the destination...
void clearCache() const override
Clears any cached parameters associated with the geometry, e.g., bounding boxes.
Definition qgscurve.cpp:294
QgsBox3D mBoundingBox
Cached bounding box.
Definition qgscurve.h:399
QgsCurve()=default
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
static QStringList wktGetChildBlocks(const QString &wkt, const QString &defaultType=QString())
Parses a WKT string and returns of list of blocks contained in the WKT.
static QPair< Qgis::WkbType, QString > wktReadBlock(const QString &wkt)
Parses a WKT block of the format "TYPE( contents )" and returns a pair of geometry type to contents (...
Line string geometry type, with support for z-dimension and m-values.
std::tuple< std::unique_ptr< QgsCurve >, std::unique_ptr< QgsCurve > > splitCurveAtVertex(int index) const override
Splits the curve at the specified vertex index, returning two curves which represent the portion of t...
double segmentLength(QgsVertexId startVertex) const override
Returns the length of the segment of the geometry which begins at startVertex.
QgsPoint startPoint() const override
Returns the starting point of the curve.
bool dropZValue() override
Drops any z-dimensions which exist in the geometry.
QPolygonF asQPolygonF() const override
Returns a QPolygonF representing the points.
void addToPainterPath(QPainterPath &path) const override
Adds a curve to a painter path.
bool hasCurvedSegments() const override
Returns true if the geometry contains curved segments.
bool isRational() const
Returns true if this curve is rational (has non-uniform weights).
bool addZValue(double zValue=0) override
Adds a z-dimension to the geometry, initialized to a preset value.
QgsNurbsCurve()
Constructor for an empty NURBS curve geometry.
QgsPoint endPoint() const override
Returns the end point of the curve.
int indexOf(const QgsPoint &point) const override
Returns the index of the first vertex matching the given point, or -1 if a matching vertex is not fou...
bool transform(QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback=nullptr) override
Transforms the vertices from the geometry in place, using the specified geometry transformer object.
QgsPoint centroid() const override
Returns the centroid of the geometry.
bool equals(const QgsCurve &other) const override
Checks whether this curve exactly equals another curve.
QgsCurve * curveSubstring(double startDistance, double endDistance) const override
Returns a new curve representing a substring of this curve.
int partCount() const override
Returns count of parts contained in the geometry.
bool addMValue(double mValue=0) override
Adds a measure to the geometry, initialized to a preset value.
bool fuzzyDistanceEqual(const QgsAbstractGeometry &other, double epsilon=1e-8) const override
Performs fuzzy distance comparison between this geometry and other using an epsilon.
QgsAbstractGeometry * simplifyByDistance(double tolerance) const override
Simplifies the geometry by applying the Douglas Peucker simplification by distance algorithm.
double zAt(int index) const override
Returns the z-coordinate of the specified node in the line string.
void sumUpArea(double &sum) const override
Sums up the area of the curve by iterating over the vertices (shoelace formula).
QString geometryType() const override
Returns a unique string representing the geometry type.
QString asKml(int precision=17) const override
Returns a KML representation of the geometry.
QgsLineString * curveToLine(double tolerance=M_PI_2/90, SegmentationToleranceType toleranceType=MaximumAngle) const override
Returns a new line string geometry corresponding to a segmentized approximation of the curve.
static QVector< double > generateKnotsForBezierConversion(int nAnchors, int degree=3)
Generates a knot vector for converting piecewise Bézier curves to NURBS.
bool dropMValue() override
Drops any measure values which exist in the geometry.
json asJsonObject(int precision=17) const override
Returns a json object representation of the geometry.
QVector< double > knots() const
Returns the knot vector of the NURBS curve.
void scroll(int firstVertexIndex) override
Scrolls the curve vertices so that they start with the vertex at the given index.
bool isEmpty() const override
Returns true if the geometry is empty.
bool isAnchorVertex(int localIndex) const
Returns true if the control point at localIndex is an anchor vertex in a poly-Bézier curve.
bool isPolyBezier() const
Returns true if this curve represents a poly-Bézier curve structure.
void filterVertices(const std::function< bool(const QgsPoint &)> &filter) override
bool isBSpline() const
Returns true if this curve represents a B-spline (non-rational NURBS).
bool fromWkb(QgsConstWkbPtr &wkb) override
Sets the geometry from a WKB string.
int numPoints() const override
Returns the number of points in the curve.
void clear() override
Clears the geometry, ie reset it to a null geometry.
QString asWkt(int precision=17) const override
Returns a WKT representation of the geometry.
QgsBox3D calculateBoundingBox3D() const override
Calculates the minimal 3D bounding box for the geometry.
QgsBox3D boundingBox3D() const override
Returns the 3D bounding box for the geometry.
bool isClosed() const override
Returns true if the curve is closed.
double yAt(int index) const override
Returns the y-coordinate of the specified node in the line string.
int wkbSize(QgsAbstractGeometry::WkbFlags flags=QgsAbstractGeometry::WkbFlags()) const override
Returns the length of the QByteArray returned by asWkb().
QgsAbstractGeometry * createEmptyWithSameType() const override
Creates a new geometry with the same class and same WKB type as the original and transfers ownership.
bool moveVertex(QgsVertexId position, const QgsPoint &newPos) override
Moves a vertex within the geometry.
int vertexNumberFromVertexId(QgsVertexId id) const override
Returns the vertex number corresponding to a vertex id.
void swapXy() override
Swaps the x and y coordinates from the geometry.
void points(QgsPointSequence &pts) const override
Returns a list of points within the curve.
QDomElement asGml2(QDomDocument &doc, int precision=17, const QString &ns="gml", QgsAbstractGeometry::AxisOrder axisOrder=QgsAbstractGeometry::AxisOrder::XY) const override
Returns a GML2 representation of the geometry.
QDomElement asGml3(QDomDocument &doc, int precision=17, const QString &ns="gml", QgsAbstractGeometry::AxisOrder axisOrder=QgsAbstractGeometry::AxisOrder::XY) const override
Returns a GML3 representation of the geometry.
bool setWeight(int index, double weight)
Sets the weight at the specified control point index.
bool fuzzyEqual(const QgsAbstractGeometry &other, double epsilon=1e-8) const override
Performs fuzzy comparison between this geometry and other using an epsilon.
QgsCurve * reversed() const override
Returns a reversed copy of the curve, where the direction of the curve has been flipped.
double mAt(int index) const override
Returns the m-coordinate of the specified node in the line string.
void drawAsPolygon(QPainter &p) const override
Draws the curve as a polygon on the specified QPainter.
bool boundingBoxIntersects(const QgsRectangle &rectangle) const override
Returns true if the bounding box of this geometry intersects with a rectangle.
double weight(int index) const
Returns the weight at the specified control point index.
void draw(QPainter &p) const override
Draws the geometry using the specified QPainter.
QByteArray asWkb(QgsAbstractGeometry::WkbFlags flags=QgsAbstractGeometry::WkbFlags()) const override
Returns a WKB representation of the geometry.
bool pointAt(int node, QgsPoint &point, Qgis::VertexType &type) const override
Returns the point and vertex id of a point within the curve.
int degree() const
Returns the degree of the NURBS curve.
double closestSegment(const QgsPoint &pt, QgsPoint &segmentPt, QgsVertexId &vertexAfter, int *leftOf=nullptr, double epsilon=4 *std::numeric_limits< double >::epsilon()) const override
Searches for the closest segment of the geometry to a given point.
void sumUpArea3D(double &sum) const override
Sums up the 3d area of the curve by iterating over the vertices (shoelace formula).
bool removeDuplicateNodes(double epsilon=4 *std::numeric_limits< double >::epsilon(), bool useZValues=false) override
Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a degenerat...
bool deleteVertex(QgsVertexId position) override
Deletes a vertex within the geometry.
static QVector< double > generateUniformKnots(int numControlPoints, int degree)
Generates a uniform clamped knot vector for a NURBS curve.
QVector< QgsPoint > controlPoints() const
Returns the control points of the NURBS curve.
double xAt(int index) const override
Returns the x-coordinate of the specified node in the line string.
bool isBezier() const
Returns true if this curve represents a Bézier curve.
QgsRectangle boundingBox() const override
Returns the minimal bounding box for the geometry.
bool isClosed2D() const override
Returns true if the curve is closed.
QgsCurve * toCurveType() const override
Returns the geometry converted to the more generic curve type.
double vertexAngle(QgsVertexId vertex) const override
Returns approximate angle at a vertex.
bool insertVertex(QgsVertexId position, const QgsPoint &vertex) override
Inserts a vertex into the geometry.
int compareToSameClass(const QgsAbstractGeometry *other) const final
Compares to an other geometry of the same class, and returns a integer for sorting of the two geometr...
QgsPoint * interpolatePoint(double distance) const override
Returns an interpolated point on the curve at the specified distance.
double length() const override
Returns the planar, 2-dimensional length of the geometry.
QgsPoint evaluate(double t) const
Evaluates the NURBS curve at parameter t ∈ [0,1].
int dimension() const override
Returns the inherent dimension of the geometry.
QVector< double > weights() const
Returns the weight vector of the NURBS curve.
QgsAbstractGeometry * snappedToGrid(double hSpacing, double vSpacing, double dSpacing=0, double mSpacing=0, bool removeRedundantPoints=false) const override
Makes a new geometry with all the points or vertices snapped to the closest point of the grid.
bool fromWkt(const QString &wkt) override
Sets the geometry from a WKT string.
bool isValid(QString &error, Qgis::GeometryValidityFlags flags=Qgis::GeometryValidityFlags()) const override
Checks validity of the geometry, and returns true if the geometry is valid.
QgsNurbsCurve * clone() const override
Clones the geometry by performing a deep copy.
QgsPoint vertexAt(QgsVertexId id) const override
Returns the point corresponding to a specified vertex id.
void clearCache() const override
Clears any cached parameters associated with the geometry, e.g., bounding boxes.
double distanceBetweenVertices(QgsVertexId fromVertex, QgsVertexId toVertex) const override
Returns the distance along the curve between two vertices.
int vertexCount(int part=0, int ring=0) const override
Returns the number of vertices of which this geometry is built.
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
void setY(double y)
Sets the point's y-coordinate.
Definition qgspoint.h:370
void setX(double x)
Sets the point's x-coordinate.
Definition qgspoint.h:359
double z
Definition qgspoint.h:58
double x
Definition qgspoint.h:56
void setM(double m)
Sets the point's m-value.
Definition qgspoint.h:398
bool isEmpty() const override
Returns true if the geometry is empty.
Definition qgspoint.cpp:760
void setZ(double z)
Sets the point's z-coordinate.
Definition qgspoint.h:383
double m
Definition qgspoint.h:59
double y
Definition qgspoint.h:57
A rectangle specified with double values.
bool intersects(const QgsRectangle &rect) const
Returns true when rectangle intersects with other rectangle.
WKB pointer handler.
Definition qgswkbptr.h:45
static Qgis::WkbType dropM(Qgis::WkbType type)
Drops the m dimension (if present) for a WKB type and returns the new type.
static Qgis::WkbType dropZ(Qgis::WkbType type)
Drops the z dimension (if present) for a WKB type and returns the new type.
static Qgis::WkbType addM(Qgis::WkbType type)
Adds the m dimension to a WKB type and returns the new type.
static Qgis::WkbType addZ(Qgis::WkbType type)
Adds the z dimension to a WKB type and returns the new type.
static Q_INVOKABLE bool hasZ(Qgis::WkbType type)
Tests whether a WKB type contains the z-dimension.
static Q_INVOKABLE bool hasM(Qgis::WkbType type)
Tests whether a WKB type contains m values.
static Q_INVOKABLE bool isNurbsType(Qgis::WkbType type)
Returns true if the WKB type is a NURBS curve type.
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition qgis.h:6867
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6950
T qgsgeometry_cast(QgsAbstractGeometry *geom)
QVector< QgsPoint > QgsPointSequence
Utility class for identifying a unique vertex within a geometry.
Definition qgsvertexid.h:34
int vertex
Vertex number.
Definition qgsvertexid.h:98
int part
Part number.
Definition qgsvertexid.h:92
int ring
Ring number.
Definition qgsvertexid.h:95