QGIS API Documentation 4.1.0-Master (60fea48833c)
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>> QgsNurbsCurve::splitCurveAtVertex( int index ) const
521{
522 std::unique_ptr<QgsLineString> line( curveToLine() );
523 if ( !line )
524 {
525 return std::make_tuple( nullptr, nullptr );
526 }
527 return line->splitCurveAtVertex( index );
528}
529
531{
532 return mControlPoints.isEmpty() ? QgsPoint() : mControlPoints.first();
533}
534
535void QgsNurbsCurve::sumUpArea( double &sum ) const
536{
537 // TODO - investigate whether this can be calculated directly
538 std::unique_ptr<QgsLineString> line( curveToLine() );
539 if ( line )
540 line->sumUpArea( sum );
541}
542
543void QgsNurbsCurve::sumUpArea3D( double &sum ) const
544{
545 std::unique_ptr<QgsLineString> line( curveToLine() );
546 if ( line )
547 line->sumUpArea3D( sum );
548}
549
550double QgsNurbsCurve::xAt( int index ) const
551{
552 if ( index < 0 || index >= mControlPoints.size() )
553 return 0.0;
554 return mControlPoints[index].x();
555}
556
557double QgsNurbsCurve::yAt( int index ) const
558{
559 if ( index < 0 || index >= mControlPoints.size() )
560 return 0.0;
561 return mControlPoints[index].y();
562}
563
564double QgsNurbsCurve::zAt( int index ) const
565{
566 if ( index < 0 || index >= mControlPoints.size() )
567 return 0.0;
568 return mControlPoints[index].is3D() ? mControlPoints[index].z() : std::numeric_limits<double>::quiet_NaN();
569}
570
571double QgsNurbsCurve::mAt( int index ) const
572{
573 if ( index < 0 || index >= mControlPoints.size() )
574 return 0.0;
575 return mControlPoints[index].isMeasure() ? mControlPoints[index].m() : std::numeric_limits<double>::quiet_NaN();
576}
577
578bool QgsNurbsCurve::addZValue( double zValue )
579{
581 return false;
582
583 clearCache();
585
586 for ( QgsPoint &p : mControlPoints )
587 {
588 p.addZValue( zValue );
589 }
590
591 return true;
592}
593
594bool QgsNurbsCurve::addMValue( double mValue )
595{
597 return false;
598
599 clearCache();
601
602 for ( QgsPoint &p : mControlPoints )
603 {
604 p.addMValue( mValue );
605 }
606
607 return true;
608}
609
611{
612 if ( !is3D() )
613 return false;
614
615 for ( QgsPoint &p : mControlPoints )
616 {
617 p.setZ( std::numeric_limits<double>::quiet_NaN() );
618 }
619
621 clearCache();
622 return true;
623}
624
626{
627 if ( !isMeasure() )
628 return false;
629
630 for ( QgsPoint &p : mControlPoints )
631 {
632 p.setM( std::numeric_limits<double>::quiet_NaN() );
633 }
634
636 clearCache();
637 return true;
638}
639
641{
642 if ( position.part != 0 || position.ring != 0 )
643 {
644 return false;
645 }
646 const int idx = position.vertex;
647 if ( idx < 0 || idx >= mControlPoints.size() )
648 {
649 return false;
650 }
651 mControlPoints.remove( idx );
652 if ( idx < mWeights.size() )
653 {
654 mWeights.remove( idx );
655 }
656
658
659 clearCache();
660 return true;
661}
662
663void QgsNurbsCurve::filterVertices( const std::function<bool( const QgsPoint & )> &filter )
664{
665 QVector<QgsPoint> newPts;
666 QVector<double> newWeights;
667 for ( int i = 0; i < mControlPoints.size(); ++i )
668 {
669 if ( filter( mControlPoints[i] ) )
670 {
671 newPts.append( mControlPoints[i] );
672 if ( i < mWeights.size() )
673 newWeights.append( mWeights[i] );
674 }
675 }
676 mControlPoints = newPts;
677 mWeights = newWeights;
678
680
681 clearCache();
682}
683
684QVector<double> QgsNurbsCurve::generateUniformKnots( int numControlPoints, int degree )
685{
686 Q_ASSERT( numControlPoints > degree );
687
688 const int knotsSize = numControlPoints + degree + 1;
689 QVector<double> knots;
690 knots.reserve( knotsSize );
691 for ( int i = 0; i < knotsSize; ++i )
692 {
693 if ( i <= degree )
694 knots.append( 0.0 );
695 else if ( i >= numControlPoints )
696 knots.append( 1.0 );
697 else
698 knots.append( static_cast<double>( i - degree ) / ( numControlPoints - degree ) );
699 }
700 return knots;
701}
702
703QVector<double> QgsNurbsCurve::generateKnotsForBezierConversion( int nAnchors, int degree )
704{
705 if ( nAnchors < 2 || degree < 1 )
706 return QVector<double>();
707
708 const int segmentCount = nAnchors - 1;
709 const int totalKnots = degree * nAnchors + degree + 1;
710
711 QVector<double> knots;
712 knots.reserve( totalKnots );
713
714 // Clamping start: (degree + 1) knots at 0
715 for ( int i = 0; i < degree + 1; ++i )
716 knots.append( 0.0 );
717
718 // Interior: multiplicity 'degree' at each junction
719 for ( int segmentIndex = 1; segmentIndex < segmentCount; ++segmentIndex )
720 {
721 for ( int j = 0; j < degree; ++j )
722 knots.append( static_cast<double>( segmentIndex ) );
723 }
724
725 // Clamping end: (degree + 1) knots at segmentCount
726 for ( int i = 0; i < degree + 1; ++i )
727 knots.append( static_cast<double>( segmentCount ) );
728
729 return knots;
730}
731
733{
734 mKnots = generateUniformKnots( mControlPoints.size(), mDegree );
735}
736
738{
739 clear();
740
741 if ( !wkb )
742 return false;
743
744 // Store header endianness
745 const unsigned char headerEndianness = *static_cast<const unsigned char *>( wkb );
746
747 Qgis::WkbType type = wkb.readHeader();
748 if ( !QgsWkbTypes::isNurbsType( type ) )
749 return false;
750
751 mWkbType = type;
752 const bool is3D = QgsWkbTypes::hasZ( type );
753 const bool isMeasure = QgsWkbTypes::hasM( type );
754
755 // Read degree (4 bytes uint32)
756 quint32 degree;
757 wkb >> degree;
758
759 // Validate degree before casting to int
760 if ( degree < 1 || degree > static_cast<quint32>( std::numeric_limits<int>::max() ) )
761 return false;
762
763 mDegree = static_cast<int>( degree );
764
765 // Read number of control points (4 bytes uint32)
766 quint32 numControlPoints;
767 wkb >> numControlPoints;
768
769 // Sanity check: numControlPoints should be reasonable given the WKB blob size
770 // Each control point needs at least:
771 // - 1 byte (endianness)
772 // - 16 bytes (x,y)
773 // - 8 bytes (z) if 3D
774 // - 8 bytes (m) if measure
775 // - 1 byte (weight flag)
776 // Minimum: 18 bytes (2D) to 34 bytes (ZM)
777 const int minBytesPerPoint = 18 + ( is3D ? 8 : 0 ) + ( isMeasure ? 8 : 0 );
778 if ( numControlPoints > static_cast<quint32>( wkb.remaining() / minBytesPerPoint + 1 ) )
779 return false;
780
781 mControlPoints.clear();
782 mWeights.clear();
783 mControlPoints.reserve( numControlPoints );
784 mWeights.reserve( numControlPoints );
785
786 // Read control points
787 for ( quint32 i = 0; i < numControlPoints; ++i )
788 {
789 // Read byte order for this point (1 byte)
790 char pointEndianness;
791 wkb >> pointEndianness;
792
793 // Validate endianness: must be 0 (big-endian) or 1 (little-endian)
794 // and must match the WKB header endianness
795 if ( static_cast<unsigned char>( pointEndianness ) != headerEndianness )
796 return false;
797
798 // Read coordinates
799 double x, y, z = 0.0, m = 0.0;
800 wkb >> x >> y;
801
802 if ( is3D )
803 wkb >> z;
804 if ( isMeasure )
805 wkb >> m;
806
807 // Read weight flag (1 byte)
808 char weightFlag;
809 wkb >> weightFlag;
810
811 double weight = 1.0;
812 if ( weightFlag == 1 )
813 {
814 // Read custom weight (8 bytes double)
815 wkb >> weight;
816 }
817
818 // Create point with appropriate dimensionality
819 QgsPoint point;
820 if ( is3D && isMeasure )
821 point = QgsPoint( x, y, z, m );
822 else if ( is3D )
823 point = QgsPoint( x, y, z );
824 else if ( isMeasure )
825 point = QgsPoint( x, y, std::numeric_limits<double>::quiet_NaN(), m );
826 else
827 point = QgsPoint( x, y );
828
829 mControlPoints.append( point );
830 mWeights.append( weight );
831 }
832
833 // Read number of knots (4 bytes uint32)
834 quint32 numKnots;
835 wkb >> numKnots;
836
837 // Sanity check: numKnots should be numControlPoints + degree + 1
838 const quint32 expectedKnots = numControlPoints + degree + 1;
839 if ( numKnots != expectedKnots )
840 return false;
841
842 // Sanity check: remaining WKB should have enough bytes for knots
843 if ( numKnots * sizeof( double ) > static_cast<quint32>( wkb.remaining() ) )
844 return false;
845
846 mKnots.clear();
847 mKnots.reserve( numKnots );
848
849 // Read knot values (8 bytes double each)
850 for ( quint32 i = 0; i < numKnots; ++i )
851 {
852 double knot;
853 wkb >> knot;
854 mKnots.append( knot );
855 }
856
857 return true;
858}
859
860bool QgsNurbsCurve::fromWkt( const QString &wkt )
861{
862 clear();
863
864 const QString geomTypeStr = wkt.split( '(' )[0].trimmed().toUpper();
865
866 if ( !geomTypeStr.startsWith( "NURBSCURVE"_L1 ) )
867 {
868 return false;
869 }
870
871 // Determine dimensionality from the geometry type string
872 // Handle both "NURBSCURVEZM" and "NURBSCURVE ZM" formats
873 if ( geomTypeStr.contains( "ZM"_L1 ) )
875 else if ( geomTypeStr.endsWith( 'Z' ) || geomTypeStr.endsWith( " Z"_L1 ) )
877 else if ( geomTypeStr.endsWith( 'M' ) || geomTypeStr.endsWith( " M"_L1 ) )
879 else
881
882 QPair<Qgis::WkbType, QString> parts = QgsGeometryUtils::wktReadBlock( wkt );
883
884 if ( parts.second.compare( "EMPTY"_L1, Qt::CaseInsensitive ) == 0 || parts.second.isEmpty() )
885 return true;
886
887 // Split the content by commas at parentheses level 0
888 QStringList blocks = QgsGeometryUtils::wktGetChildBlocks( parts.second, QString() );
889
890 if ( blocks.isEmpty() )
891 return false;
892
893 // First block should be the degree
894 bool ok = true;
895 int degree = blocks[0].trimmed().toInt( &ok );
896 if ( !ok || degree < 1 )
897 return false;
898
899 if ( blocks.size() < 2 )
900 return false;
901
902 // Second block should be the control points
903 QString pointsStr = blocks[1].trimmed();
904
905 // Validate control points block starts with '(' and ends with ')'
906 if ( !pointsStr.startsWith( '('_L1 ) || !pointsStr.endsWith( ')'_L1 ) )
907 return false;
908
909 pointsStr = pointsStr.mid( 1, pointsStr.length() - 2 ).trimmed();
910
911 // Parse control points
912 QStringList pointsCoords = pointsStr.split( ',', Qt::SkipEmptyParts );
913 QVector<QgsPoint> controlPoints;
914
915 const thread_local QRegularExpression rx( u"\\s+"_s );
916
917 for ( const QString &pointStr : pointsCoords )
918 {
919 QStringList coords = pointStr.trimmed().split( rx, Qt::SkipEmptyParts );
920
921 if ( coords.size() < 2 )
922 return false;
923
924 QgsPoint point;
925 bool ok = true;
926
927 double x = coords[0].toDouble( &ok );
928 if ( !ok )
929 return false;
930
931 double y = coords[1].toDouble( &ok );
932 if ( !ok )
933 return false;
934
935 // Handle different coordinate patterns based on declared geometry type
936 if ( coords.size() >= 3 )
937 {
938 if ( isMeasure() && !is3D() && coords.size() == 3 )
939 {
940 // NURBSCURVE M pattern: (x y m) - third coordinate is M, not Z
941 double m = coords[2].toDouble( &ok );
942 if ( !ok )
943 return false;
944 point = QgsPoint( x, y, std::numeric_limits<double>::quiet_NaN(), m );
945 }
946 else if ( is3D() && !isMeasure() && coords.size() >= 3 )
947 {
948 // NURBSCURVE Z pattern: (x y z)
949 double z = coords[2].toDouble( &ok );
950 if ( !ok )
951 return false;
952 point = QgsPoint( x, y, z );
953 }
954 else if ( is3D() && isMeasure() && coords.size() >= 4 )
955 {
956 // NURBSCURVE ZM pattern: (x y z m)
957 double z = coords[2].toDouble( &ok );
958 if ( !ok )
959 return false;
960 double m = coords[3].toDouble( &ok );
961 if ( !ok )
962 return false;
963 point = QgsPoint( x, y, z, m );
964 }
965 else if ( isMeasure() && coords.size() >= 4 )
966 {
967 // NURBSCURVE M pattern with 4 coords: (x y z m) - upgrade to ZM
968 double z = coords[2].toDouble( &ok );
969 if ( !ok )
970 return false;
971 double m = coords[3].toDouble( &ok );
972 if ( !ok )
973 return false;
974 point = QgsPoint( x, y, z, m );
975 if ( !is3D() )
977 }
978 else if ( !is3D() && !isMeasure() && coords.size() == 3 )
979 {
980 // No explicit dimension - auto-upgrade to 3D: (x y z)
981 double z = coords[2].toDouble( &ok );
982 if ( !ok )
983 return false;
984 point = QgsPoint( x, y, z );
986 }
987 else if ( !is3D() && !isMeasure() && coords.size() >= 4 )
988 {
989 // No explicit dimension - auto-upgrade to ZM: (x y z m)
990 double z = coords[2].toDouble( &ok );
991 if ( !ok )
992 return false;
993 double m = coords[3].toDouble( &ok );
994 if ( !ok )
995 return false;
996 point = QgsPoint( x, y, z, m );
999 }
1000 else
1001 {
1002 // Only 2 coordinates but declared type may require Z or M
1003 if ( is3D() && isMeasure() )
1004 point = QgsPoint( Qgis::WkbType::PointZM, x, y, 0.0, 0.0 );
1005 else if ( is3D() )
1006 point = QgsPoint( Qgis::WkbType::PointZ, x, y, 0.0 );
1007 else if ( isMeasure() )
1008 point = QgsPoint( Qgis::WkbType::PointM, x, y, std::numeric_limits<double>::quiet_NaN(), 0.0 );
1009 else
1010 point = QgsPoint( x, y );
1011 }
1012 }
1013 else
1014 {
1015 // Only 2 coordinates - create point matching declared type
1016 if ( is3D() && isMeasure() )
1017 point = QgsPoint( Qgis::WkbType::PointZM, x, y, 0.0, 0.0 );
1018 else if ( is3D() )
1019 point = QgsPoint( Qgis::WkbType::PointZ, x, y, 0.0 );
1020 else if ( isMeasure() )
1021 point = QgsPoint( Qgis::WkbType::PointM, x, y, std::numeric_limits<double>::quiet_NaN(), 0.0 );
1022 else
1023 point = QgsPoint( x, y );
1024 }
1025
1026 controlPoints.append( point );
1027 }
1028
1029 mControlPoints = controlPoints;
1030
1031 // Initialize weights to 1.0 (non-rational by default)
1032 mWeights.clear();
1033 for ( int i = 0; i < controlPoints.size(); ++i )
1034 {
1035 mWeights.append( 1.0 );
1036 }
1037
1038 // Parse additional parameters (degree already parsed at the beginning)
1039 bool hasWeights = false;
1040 bool hasKnots = false;
1041
1042 // Process remaining blocks (starting from index 2 since 0=degree, 1=control points)
1043 for ( int i = 2; i < blocks.size(); ++i )
1044 {
1045 QString block = blocks[i].trimmed();
1046
1047 if ( block.startsWith( '('_L1 ) )
1048 {
1049 // Validate block ends with ')'
1050 if ( !block.endsWith( ')'_L1 ) )
1051 return false;
1052
1053 // This could be weights or knots vector
1054 block = block.mid( 1, block.length() - 2 ).trimmed();
1055 QStringList values = block.split( ',', Qt::SkipEmptyParts );
1056
1057 QVector<double> parsedValues;
1058 for ( const QString &valueStr : values )
1059 {
1060 bool ok = true;
1061 double value = valueStr.trimmed().toDouble( &ok );
1062 if ( !ok )
1063 return false;
1064 parsedValues.append( value );
1065 }
1066
1067 if ( !hasWeights && parsedValues.size() == controlPoints.size() )
1068 {
1069 // This is the weights vector
1070 mWeights = parsedValues;
1071 hasWeights = true;
1072 }
1073 else if ( !hasKnots )
1074 {
1075 // This is the knots vector
1076 mKnots = parsedValues;
1077 hasKnots = true;
1078 }
1079 }
1080 else
1081 {
1082 // Invalid block - doesn't start with '('
1083 return false;
1084 }
1085 }
1086
1087 mDegree = degree;
1088
1089 // Validate: need at least (degree + 1) control points
1090 if ( controlPoints.size() <= degree )
1091 return false;
1092
1093 // If no knots were provided, create default knots (open uniform)
1094 if ( !hasKnots )
1095 {
1097 }
1098
1099 return true;
1100}
1101
1102bool QgsNurbsCurve::fuzzyEqual( const QgsAbstractGeometry &other, double epsilon ) const
1103{
1105 if ( !o )
1106 return false;
1107
1108 if ( mDegree != o->mDegree || mControlPoints.size() != o->mControlPoints.size() || mWeights.size() != o->mWeights.size() || mKnots.size() != o->mKnots.size() )
1109 {
1110 return false;
1111 }
1112
1113 for ( int i = 0; i < mControlPoints.size(); ++i )
1114 {
1115 if ( mControlPoints[i].distance( o->mControlPoints[i] ) >= epsilon )
1116 return false;
1117 }
1118
1119 for ( int i = 0; i < mWeights.size(); ++i )
1120 {
1121 if ( std::fabs( mWeights[i] - o->mWeights[i] ) > epsilon )
1122 return false;
1123 }
1124
1125 for ( int i = 0; i < mKnots.size(); ++i )
1126 {
1127 if ( std::fabs( mKnots[i] - o->mKnots[i] ) > epsilon )
1128 return false;
1129 }
1130
1131 return true;
1132}
1133
1134bool QgsNurbsCurve::fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon ) const
1135{
1136 return fuzzyEqual( other, epsilon );
1137}
1138
1140{
1141 return u"NurbsCurve"_s;
1142}
1143
1145{
1146 return true;
1147}
1148
1150{
1151 return 1;
1152}
1153
1155{
1156 return new QgsNurbsCurve( *this );
1157}
1158
1160{
1161 if ( id.part != 0 || id.ring != 0 )
1162 {
1163 return QgsPoint();
1164 }
1165 const int idx = id.vertex;
1166 if ( idx < 0 || idx >= mControlPoints.size() )
1167 {
1168 return QgsPoint();
1169 }
1170 return mControlPoints[idx];
1171}
1172
1173int QgsNurbsCurve::vertexCount( int part, int ring ) const
1174{
1175 return ( part == 0 && ring == 0 ) ? mControlPoints.size() : 0;
1176}
1177
1179{
1180 if ( id.part == 0 && id.ring == 0 )
1181 {
1182 return id.vertex;
1183 }
1184 return -1;
1185}
1186
1187bool QgsNurbsCurve::isValid( QString &error, Qgis::GeometryValidityFlags flags ) const
1188{
1189 Q_UNUSED( flags );
1190
1191 // Use cached validity if available
1192 if ( mValidityComputed )
1193 {
1194 if ( !mIsValid )
1195 error = u"NURBS curve is invalid"_s;
1196 return mIsValid;
1197 }
1198
1199 mValidityComputed = true;
1200 mIsValid = false;
1201
1202 if ( mDegree < 1 )
1203 {
1204 error = u"Degree must be >= 1"_s;
1205 return false;
1206 }
1207
1208 const int n = mControlPoints.size();
1209 if ( n < mDegree + 1 )
1210 {
1211 error = u"Not enough control points for degree"_s;
1212 return false;
1213 }
1214
1215 if ( mKnots.size() != n + mDegree + 1 )
1216 {
1217 error = u"Knot vector size is incorrect"_s;
1218 return false;
1219 }
1220
1221 if ( mWeights.size() != n )
1222 {
1223 error = u"Weights vector size mismatch"_s;
1224 return false;
1225 }
1226
1227 // Check that knots are non-decreasing
1228 for ( int i = 1; i < mKnots.size(); ++i )
1229 {
1230 if ( mKnots[i] < mKnots[i - 1] )
1231 {
1232 error = u"Knot vector values must be non-decreasing"_s;
1233 return false;
1234 }
1235 }
1236
1237 mIsValid = true;
1238 return true;
1239}
1240
1241void QgsNurbsCurve::addToPainterPath( QPainterPath &path ) const
1242{
1243 std::unique_ptr<QgsLineString> line( curveToLine() );
1244 if ( line )
1245 line->addToPainterPath( path );
1246}
1247
1248QgsCurve *QgsNurbsCurve::curveSubstring( double startDistance, double endDistance ) const
1249{
1250 std::unique_ptr<QgsLineString> line( curveToLine() );
1251 if ( !line )
1252 return nullptr;
1253 return line->curveSubstring( startDistance, endDistance );
1254}
1255
1257{
1258 std::unique_ptr<QgsLineString> line( curveToLine() );
1259 return line ? line->length() : 0.0;
1260}
1261
1263{
1264 std::unique_ptr<QgsLineString> line( curveToLine() );
1265 if ( !line )
1266 return 0.0;
1267 return line->segmentLength( startVertex );
1268}
1269
1271{
1272 std::unique_ptr<QgsLineString> line( curveToLine() );
1273 if ( !line )
1274 return -1.0;
1275 return line->distanceBetweenVertices( fromVertex, toVertex );
1276}
1277
1278QgsAbstractGeometry *QgsNurbsCurve::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const
1279{
1280 auto result = new QgsNurbsCurve( *this );
1281 for ( QgsPoint &pt : result->mControlPoints )
1282 {
1283 if ( hSpacing > 0 )
1284 pt.setX( std::round( pt.x() / hSpacing ) * hSpacing );
1285 if ( vSpacing > 0 )
1286 pt.setY( std::round( pt.y() / vSpacing ) * vSpacing );
1287 if ( pt.is3D() && dSpacing > 0 )
1288 pt.setZ( std::round( pt.z() / dSpacing ) * dSpacing );
1289 if ( pt.isMeasure() && mSpacing > 0 )
1290 pt.setM( std::round( pt.m() / mSpacing ) * mSpacing );
1291 }
1292
1293 if ( removeRedundantPoints )
1294 result->removeDuplicateNodes();
1295
1296 return result;
1297}
1298
1300{
1301 std::unique_ptr<QgsLineString> line( curveToLine() );
1302 if ( !line )
1303 return new QgsNurbsCurve( *this );
1304 return line->simplifyByDistance( tolerance );
1305}
1306
1307bool QgsNurbsCurve::removeDuplicateNodes( double epsilon, bool useZValues )
1308{
1309 if ( mControlPoints.size() < 2 )
1310 return false;
1311
1312 QVector<QgsPoint> newPoints;
1313 QVector<double> newWeights;
1314
1315 newPoints.reserve( mControlPoints.size() );
1316 newWeights.reserve( mWeights.size() );
1317
1318 newPoints.append( mControlPoints.first() );
1319 if ( !mWeights.isEmpty() )
1320 newWeights.append( mWeights.first() );
1321
1322 for ( int i = 1; i < mControlPoints.size(); ++i )
1323 {
1324 const double dist = ( useZValues && mControlPoints[i].is3D() && mControlPoints[i - 1].is3D() ) ? mControlPoints[i].distance3D( mControlPoints[i - 1] )
1325 : mControlPoints[i].distance( mControlPoints[i - 1] );
1326
1327 if ( dist >= epsilon )
1328 {
1329 newPoints.append( mControlPoints[i] );
1330 if ( i < mWeights.size() )
1331 newWeights.append( mWeights[i] );
1332 }
1333 }
1334
1335 const bool changed = ( newPoints.size() != mControlPoints.size() );
1336 if ( !changed )
1337 return false;
1338
1339 mControlPoints = newPoints;
1340 mWeights = newWeights;
1341
1342 // Regenerate uniform knot vector for the new number of control points
1344
1345 clearCache();
1346 return true;
1347}
1348
1350{
1351 std::unique_ptr<QgsLineString> line( curveToLine() );
1352 if ( !line )
1353 return 0.0;
1354 return line->vertexAngle( vertex );
1355}
1356
1358{
1359 for ( QgsPoint &pt : mControlPoints )
1360 {
1361 const double x = pt.x();
1362 pt.setX( pt.y() );
1363 pt.setY( x );
1364 }
1365 clearCache();
1366}
1367
1369{
1370 Q_UNUSED( feedback );
1371 if ( !transformer )
1372 return false;
1373
1374 for ( QgsPoint &pt : mControlPoints )
1375 {
1376 double x = pt.x(), y = pt.y(), z = pt.z(), m = pt.m();
1377 if ( !transformer->transformPoint( x, y, z, m ) )
1378 return false;
1379 pt.setX( x );
1380 pt.setY( y );
1381 pt.setZ( z );
1382 pt.setM( m );
1383 }
1384
1385 clearCache();
1386 return true;
1387}
1388
1393
1394double QgsNurbsCurve::closestSegment( const QgsPoint &pt, QgsPoint &segmentPt, QgsVertexId &vertexAfter, int *leftOf, double epsilon ) const
1395{
1396 std::unique_ptr<QgsLineString> line( curveToLine() );
1397 if ( !line )
1398 {
1399 segmentPt = QgsPoint();
1400 vertexAfter = QgsVertexId();
1401 if ( leftOf )
1402 *leftOf = 0;
1403 return -1;
1404 }
1405 return line->closestSegment( pt, segmentPt, vertexAfter, leftOf, epsilon );
1406}
1407
1409{
1410 for ( QgsPoint &pt : mControlPoints )
1411 {
1412 double x = pt.x();
1413 double y = pt.y();
1414 double z = transformZ && pt.is3D() ? pt.z() : std::numeric_limits<double>::quiet_NaN();
1415 ct.transformInPlace( x, y, z, d );
1416 pt.setX( x );
1417 pt.setY( y );
1418 if ( transformZ && pt.is3D() )
1419 pt.setZ( z );
1420 }
1421 clearCache();
1422}
1423
1424void QgsNurbsCurve::transform( const QTransform &t, double zTranslate, double zScale, double mTranslate, double mScale )
1425{
1426 for ( QgsPoint &pt : mControlPoints )
1427 {
1428 const QPointF p = t.map( QPointF( pt.x(), pt.y() ) );
1429 pt.setX( p.x() );
1430 pt.setY( p.y() );
1431
1432 if ( pt.is3D() )
1433 pt.setZ( pt.z() * zScale + zTranslate );
1434 if ( pt.isMeasure() )
1435 pt.setM( pt.m() * mScale + mTranslate );
1436 }
1437 clearCache();
1438}
1439
1444
1446{
1447 if ( mBoundingBox.isNull() )
1448 {
1450 }
1451 return mBoundingBox;
1452}
1453
1455{
1456 if ( mControlPoints.isEmpty() )
1457 return QgsBox3D();
1458
1459 // The bounding box must include all control points, not just points on the curve.
1460 // This is important for Snapping to control points (they can lie outside the curve itself)
1461 QgsBox3D bbox;
1462 for ( const QgsPoint &pt : mControlPoints )
1463 {
1464 bbox.combineWith( pt.x(), pt.y(), pt.is3D() ? pt.z() : std::numeric_limits<double>::quiet_NaN() );
1465 }
1466
1467 // Also include points on the curve to ensure the bbox is complete
1468 std::unique_ptr<QgsLineString> line( curveToLine() );
1469 if ( line )
1470 {
1471 bbox.combineWith( line->boundingBox3D() );
1472 }
1473
1474 return bbox;
1475}
1476
1478{
1480 mValidityComputed = false;
1481 mIsValid = false;
1482}
1483
1484bool QgsNurbsCurve::moveVertex( QgsVertexId position, const QgsPoint &newPos )
1485{
1486 if ( position.part != 0 || position.ring != 0 )
1487 return false;
1488
1489 const int idx = position.vertex;
1490 if ( idx < 0 || idx >= mControlPoints.size() )
1491 return false;
1492
1493 mControlPoints[idx].setX( newPos.x() );
1494 mControlPoints[idx].setY( newPos.y() );
1495
1496 if ( is3D() && newPos.is3D() )
1497 mControlPoints[idx].setZ( newPos.z() );
1498
1499 if ( isMeasure() && newPos.isMeasure() )
1500 mControlPoints[idx].setM( newPos.m() );
1501
1502 clearCache();
1503 return true;
1504}
1505
1506bool QgsNurbsCurve::insertVertex( QgsVertexId position, const QgsPoint &vertex )
1507{
1508 if ( position.part != 0 || position.ring != 0 )
1509 return false;
1510
1511 const int idx = position.vertex;
1512 if ( idx < 0 || idx > mControlPoints.size() )
1513 return false;
1514
1515 mControlPoints.insert( idx, vertex );
1516 if ( idx <= mWeights.size() )
1517 mWeights.insert( idx, 1.0 );
1518
1520
1521 clearCache();
1522 return true;
1523}
1524
1526{
1527 Q_UNUSED( flags );
1528
1529 const bool is3D = QgsWkbTypes::hasZ( mWkbType );
1530 const bool isMeasure = QgsWkbTypes::hasM( mWkbType );
1531 const int coordinateDimension = 2 + ( is3D ? 1 : 0 ) + ( isMeasure ? 1 : 0 );
1532
1533 int size = 0;
1534
1535 // WKB header (endianness + type)
1536 size += 1 + 4;
1537
1538 // Degree (4 bytes)
1539 size += 4;
1540
1541 // Number of control points (4 bytes)
1542 size += 4;
1543
1544 // Control points data
1545 for ( int i = 0; i < mControlPoints.size(); ++i )
1546 {
1547 // Point byte order (1 byte)
1548 size += 1;
1549
1550 // Coordinates (8 bytes per coordinate)
1551 size += coordinateDimension * 8;
1552
1553 // Weight flag (1 byte)
1554 size += 1;
1555
1556 // Weight value if not default (8 bytes)
1557 if ( i < mWeights.size() && std::fabs( mWeights[i] - 1.0 ) > 1e-10 )
1558 size += 8;
1559 }
1560
1561 // Number of knots (4 bytes)
1562 size += 4;
1563
1564 // Knot values (8 bytes each)
1565 size += mKnots.size() * 8;
1566
1567 return size;
1568}
1569
1571{
1572 QByteArray wkbArray;
1573 wkbArray.resize( QgsNurbsCurve::wkbSize( flags ) );
1574 QgsWkbPtr wkbPtr( wkbArray );
1575
1576 // Write WKB header
1577 wkbPtr << static_cast<char>( QgsApplication::endian() );
1578 wkbPtr << static_cast<quint32>( mWkbType );
1579
1580 // Write degree (4 bytes uint32)
1581 wkbPtr << static_cast<quint32>( mDegree );
1582
1583 // Write number of control points (4 bytes uint32)
1584 wkbPtr << static_cast<quint32>( mControlPoints.size() );
1585
1586 const bool is3D = QgsWkbTypes::hasZ( mWkbType );
1587 const bool isMeasure = QgsWkbTypes::hasM( mWkbType );
1588
1589 // Write control points
1590 for ( int i = 0; i < mControlPoints.size(); ++i )
1591 {
1592 const QgsPoint &point = mControlPoints[i];
1593
1594 // Write byte order for this point (1 byte) - use same as global
1595 wkbPtr << static_cast<char>( QgsApplication::endian() );
1596
1597 // Write coordinates
1598 wkbPtr << point.x() << point.y();
1599
1600 if ( is3D )
1601 wkbPtr << point.z();
1602 if ( isMeasure )
1603 wkbPtr << point.m();
1604
1605 // Write weight flag and weight
1606 const double weight = ( i < mWeights.size() ) ? mWeights[i] : 1.0;
1607 const bool hasCustomWeight = std::fabs( weight - 1.0 ) > 1e-10;
1608
1609 wkbPtr << static_cast<char>( hasCustomWeight ? 1 : 0 );
1610
1611 if ( hasCustomWeight )
1612 {
1613 wkbPtr << weight;
1614 }
1615 }
1616
1617 // Write number of knots (4 bytes uint32)
1618 wkbPtr << static_cast<quint32>( mKnots.size() );
1619
1620 // Write knot values (8 bytes double each)
1621 for ( const double knot : mKnots )
1622 {
1623 wkbPtr << knot;
1624 }
1625
1626 return wkbArray;
1627}
1628
1629QString QgsNurbsCurve::asWkt( int precision ) const
1630{
1631 QString wkt = wktTypeStr();
1632
1633 if ( isEmpty() )
1634 {
1635 wkt += " EMPTY"_L1;
1636 }
1637 else
1638 {
1639 wkt += " ("_L1;
1640
1641 // Add degree first
1642 wkt += QString::number( mDegree );
1643
1644 // Add control points
1645 wkt += ", ("_L1;
1646 for ( int i = 0; i < mControlPoints.size(); ++i )
1647 {
1648 if ( i > 0 )
1649 wkt += ", "_L1;
1650
1651 const QgsPoint &pt = mControlPoints[i];
1652 wkt += qgsDoubleToString( pt.x(), precision ) + ' ' + qgsDoubleToString( pt.y(), precision );
1653
1654 if ( pt.is3D() )
1655 wkt += ' ' + qgsDoubleToString( pt.z(), precision );
1656
1657 if ( pt.isMeasure() )
1658 wkt += ' ' + qgsDoubleToString( pt.m(), precision );
1659 }
1660 wkt += ')';
1661
1662 // Always add weights if they exist to ensure round-trip consistency
1663 if ( !mWeights.isEmpty() )
1664 {
1665 wkt += ", ("_L1;
1666 for ( int i = 0; i < mWeights.size(); ++i )
1667 {
1668 if ( i > 0 )
1669 wkt += ", "_L1;
1670 wkt += qgsDoubleToString( mWeights[i], precision );
1671 }
1672 wkt += ')';
1673 }
1674
1675 // Always add knots if they exist to ensure round-trip consistency
1676 if ( !mKnots.isEmpty() )
1677 {
1678 wkt += ", ("_L1;
1679 for ( int i = 0; i < mKnots.size(); ++i )
1680 {
1681 if ( i > 0 )
1682 wkt += ", "_L1;
1683 wkt += qgsDoubleToString( mKnots[i], precision );
1684 }
1685 wkt += ')';
1686 }
1687
1688 wkt += ')';
1689 }
1690
1691 return wkt;
1692}
1693
1694QDomElement QgsNurbsCurve::asGml2( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const
1695{
1696 // GML2 does not support NURBS curves, convert to LineString
1697 // TODO: GML3 has BSpline support, but it's not clear how it's handled elsewhere in QGIS
1698 std::unique_ptr<QgsLineString> line( curveToLine() );
1699 if ( !line )
1700 return QDomElement();
1701 return line->asGml2( doc, precision, ns, axisOrder );
1702}
1703
1704QDomElement QgsNurbsCurve::asGml3( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const
1705{
1706 // TODO: GML3 has native BSpline support (gml:BSpline), but it's not clear how it's handled elsewhere in QGIS
1707 // For now, convert to LineString for compatibility
1708 std::unique_ptr<QgsLineString> line( curveToLine() );
1709 if ( !line )
1710 return QDomElement();
1711 return line->asGml3( doc, precision, ns, axisOrder );
1712}
1713
1714json QgsNurbsCurve::asJsonObject( int precision ) const
1715{
1716 std::unique_ptr<QgsLineString> line( curveToLine() );
1717 if ( !line )
1718 return json::object();
1719 return line->asJsonObject( precision );
1720}
1721
1722QString QgsNurbsCurve::asKml( int precision ) const
1723{
1724 // KML does not support NURBS curves, convert to LineString
1725 std::unique_ptr<QgsLineString> line( curveToLine() );
1726 if ( !line )
1727 return QString();
1728 return line->asKml( precision );
1729}
1730
1732{
1733 return 1;
1734}
1735
1737{
1738 return mControlPoints.isEmpty();
1739}
1740
1742{
1743 mControlPoints.clear();
1744 mKnots.clear();
1745 mWeights.clear();
1746 mDegree = 0;
1747 clearCache();
1748}
1749
1751{
1752 return boundingBox().intersects( rectangle );
1753}
1754
1756{
1757 return boundingBox3D().intersects( box3d );
1758}
1759
1761{
1762 std::unique_ptr<QgsLineString> line( curveToLine() );
1763 return line ? line->centroid() : QgsPoint();
1764}
1765
1767{
1768 const QgsNurbsCurve *otherCurve = qgsgeometry_cast<const QgsNurbsCurve *>( other );
1769 if ( !otherCurve )
1770 return -1;
1771
1772 if ( mDegree < otherCurve->mDegree )
1773 return -1;
1774 else if ( mDegree > otherCurve->mDegree )
1775 return 1;
1776
1777 const int nThis = mControlPoints.size();
1778 const int nOther = otherCurve->mControlPoints.size();
1779
1780 if ( nThis < nOther )
1781 return -1;
1782 else if ( nThis > nOther )
1783 return 1;
1784
1785 for ( int i = 0; i < nThis; ++i )
1786 {
1787 if ( mControlPoints[i].x() < otherCurve->mControlPoints[i].x() )
1788 return -1;
1789 if ( mControlPoints[i].x() > otherCurve->mControlPoints[i].x() )
1790 return 1;
1791 if ( mControlPoints[i].y() < otherCurve->mControlPoints[i].y() )
1792 return -1;
1793 if ( mControlPoints[i].y() > otherCurve->mControlPoints[i].y() )
1794 return 1;
1795 }
1796
1797 if ( mWeights.size() < otherCurve->mWeights.size() )
1798 return -1;
1799 else if ( mWeights.size() > otherCurve->mWeights.size() )
1800 return 1;
1801
1802 for ( int i = 0; i < mWeights.size(); ++i )
1803 {
1804 if ( mWeights[i] < otherCurve->mWeights[i] )
1805 return -1;
1806 else if ( mWeights[i] > otherCurve->mWeights[i] )
1807 return 1;
1808 }
1809
1810 if ( mKnots.size() < otherCurve->mKnots.size() )
1811 return -1;
1812 else if ( mKnots.size() > otherCurve->mKnots.size() )
1813 return 1;
1814
1815 for ( int i = 0; i < mKnots.size(); ++i )
1816 {
1817 if ( mKnots[i] < otherCurve->mKnots[i] )
1818 return -1;
1819 else if ( mKnots[i] > otherCurve->mKnots[i] )
1820 return 1;
1821 }
1822
1823 return 0;
1824}
1825
1826double QgsNurbsCurve::weight( int index ) const
1827{
1828 if ( index < 0 || index >= mWeights.size() )
1829 return 1.0;
1830 return mWeights[index];
1831}
1832
1833bool QgsNurbsCurve::setWeight( int index, double weight )
1834{
1835 if ( index < 0 || index >= mWeights.size() )
1836 return false;
1837 if ( weight <= 0.0 )
1838 return false;
1839 mWeights[index] = weight;
1840 clearCache();
1841 return true;
1842}
1843
1844bool QgsNurbsCurve::isAnchorVertex( int localIndex ) const
1845{
1846 if ( !isPolyBezier() || localIndex < 0 || localIndex >= mControlPoints.size() )
1847 return false;
1848
1849 return ( localIndex % mDegree ) == 0;
1850}
QFlags< GeometryValidityFlag > GeometryValidityFlags
Geometry validity flags.
Definition qgis.h:2155
VertexType
Types of vertex.
Definition qgis.h:3179
@ ControlPoint
A NURBS control point (does not lie on the curve).
Definition qgis.h:3182
WkbType
The WKB type describes the number of dimensions a geometry has.
Definition qgis.h:294
@ NurbsCurveM
NurbsCurveM.
Definition qgis.h:344
@ NurbsCurve
NurbsCurve.
Definition qgis.h:311
@ NurbsCurveZ
NurbsCurveZ.
Definition qgis.h:328
@ PointM
PointM.
Definition qgis.h:329
@ NurbsCurveZM
NurbsCurveZM.
Definition qgis.h:360
@ PointZ
PointZ.
Definition qgis.h:313
@ PointZM
PointZM.
Definition qgis.h:345
TransformDirection
Indicates the direction (forward or inverse) of a transform.
Definition qgis.h:2764
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:147
QgsRectangle toRectangle() const
Converts the box to a 2D rectangle.
Definition qgsbox3d.h:388
void combineWith(const QgsBox3D &box)
Expands the bbox so that it covers both the original rectangle and the given rectangle.
Definition qgsbox3d.cpp:211
A const WKB pointer.
Definition qgswkbptr.h:211
int remaining() const
remaining
Definition qgswkbptr.h:292
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:293
QgsBox3D mBoundingBox
Cached bounding box.
Definition qgscurve.h:402
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:387
void setX(double x)
Sets the point's x-coordinate.
Definition qgspoint.h:376
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:415
bool isEmpty() const override
Returns true if the geometry is empty.
Definition qgspoint.cpp:766
void setZ(double z)
Sets the point's z-coordinate.
Definition qgspoint.h:400
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:47
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:6893
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6975
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:99
int part
Part number.
Definition qgsvertexid.h:93
int ring
Ring number.
Definition qgsvertexid.h:96