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