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