QGIS API Documentation  3.8.0-Zanzibar (11aff65)
qgstessellator.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgstessellator.cpp
3  --------------------------------------
4  Date : July 2017
5  Copyright : (C) 2017 by Martin Dobias
6  Email : wonder dot sk at gmail dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15
16 #include "qgstessellator.h"
17
18 #include "qgscurve.h"
19 #include "qgsgeometry.h"
20 #include "qgsmessagelog.h"
21 #include "qgsmultipolygon.h"
22 #include "qgspoint.h"
23 #include "qgspolygon.h"
24 #include "qgstriangle.h"
25 #include "qgis_sip.h"
26
27 #include "poly2tri.h"
28
29 #include <QtDebug>
30 #include <QMatrix4x4>
31 #include <QVector3D>
32 #include <algorithm>
33
34
35 static void make_quad( float x0, float y0, float z0, float x1, float y1, float z1, float height, QVector<float> &data, bool addNormals )
36 {
37  float dx = x1 - x0;
38  float dy = -( y1 - y0 );
39
40  // perpendicular vector in plane to [x,y] is [-y,x]
41  QVector3D vn( -dy, 0, dx );
42  vn = -vn;
43  vn.normalize();
44
45  // triangle 1
46  data << x0 << z0 + height << -y0;
48  data << vn.x() << vn.y() << vn.z();
49  data << x1 << z1 + height << -y1;
51  data << vn.x() << vn.y() << vn.z();
52  data << x0 << z0 << -y0;
54  data << vn.x() << vn.y() << vn.z();
55
56  // triangle 2
57  data << x0 << z0 << -y0;
59  data << vn.x() << vn.y() << vn.z();
60  data << x1 << z1 + height << -y1;
62  data << vn.x() << vn.y() << vn.z();
63  data << x1 << z1 << -y1;
65  data << vn.x() << vn.y() << vn.z();
66 }
67
68
69 QgsTessellator::QgsTessellator( double originX, double originY, bool addNormals, bool invertNormals, bool addBackFaces )
70  : mOriginX( originX )
71  , mOriginY( originY )
73  , mInvertNormals( invertNormals )
75 {
76  mStride = 3 * sizeof( float );
78  mStride += 3 * sizeof( float );
79 }
80
81
82 static bool _isRingCounterClockWise( const QgsCurve &ring )
83 {
84  double a = 0;
85  int count = ring.numPoints();
87  QgsPoint pt, ptPrev;
88  ring.pointAt( 0, ptPrev, vt );
89  for ( int i = 1; i < count + 1; ++i )
90  {
91  ring.pointAt( i % count, pt, vt );
92  a += ptPrev.x() * pt.y() - ptPrev.y() * pt.x();
93  ptPrev = pt;
94  }
95  return a > 0; // clockwise if a is negative
96 }
97
98 static void _makeWalls( const QgsCurve &ring, bool ccw, float extrusionHeight, QVector<float> &data, bool addNormals, double originX, double originY )
99 {
100  // we need to find out orientation of the ring so that the triangles we generate
101  // face the right direction
102  // (for exterior we want clockwise order, for holes we want counter-clockwise order)
103  bool is_counter_clockwise = _isRingCounterClockWise( ring );
104
106  QgsPoint pt;
107
108  QgsPoint ptPrev;
109  ring.pointAt( is_counter_clockwise == ccw ? 0 : ring.numPoints() - 1, ptPrev, vt );
110  for ( int i = 1; i < ring.numPoints(); ++i )
111  {
112  ring.pointAt( is_counter_clockwise == ccw ? i : ring.numPoints() - i - 1, pt, vt );
113  float x0 = ptPrev.x() - originX, y0 = ptPrev.y() - originY;
114  float x1 = pt.x() - originX, y1 = pt.y() - originY;
115  float z0 = std::isnan( ptPrev.z() ) ? 0 : ptPrev.z();
116  float z1 = std::isnan( pt.z() ) ? 0 : pt.z();
117
119  make_quad( x0, y0, z0, x1, y1, z1, extrusionHeight, data, addNormals );
120  ptPrev = pt;
121  }
122 }
123
124 static QVector3D _calculateNormal( const QgsCurve *curve, double originX, double originY, bool invertNormal )
125 {
127  QgsPoint pt1, pt2;
128
129  // if it is just plain 2D curve there is no need to calculate anything
130  // because it will be a flat horizontally oriented patch
131  if ( !QgsWkbTypes::hasZ( curve->wkbType() ) )
132  return QVector3D( 0, 0, 1 );
133
134  // often we have 3D coordinates, but Z is the same for all vertices
135  // so in order to save calculation and avoid possible issues with order of vertices
136  // (the calculation below may decide that a polygon faces downwards)
137  bool sameZ = true;
138  curve->pointAt( 0, pt1, vt );
139  for ( int i = 1; i < curve->numPoints(); i++ )
140  {
141  curve->pointAt( i, pt2, vt );
142  if ( pt1.z() != pt2.z() )
143  {
144  sameZ = false;
145  break;
146  }
147  }
148  if ( sameZ )
149  return QVector3D( 0, 0, 1 );
150
151  // Calculate the polygon's normal vector, based on Newell's method
152  // https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal
153  //
154  // Order of vertices is important here as it determines the front/back face of the polygon
155
156  double nx = 0, ny = 0, nz = 0;
157  for ( int i = 0; i < curve->numPoints() - 1; i++ )
158  {
159  curve->pointAt( i, pt1, vt );
160  curve->pointAt( i + 1, pt2, vt );
161
162  // shift points by the tessellator's origin - this does not affect normal calculation and it may save us from losing some precision
163  pt1.setX( pt1.x() - originX );
164  pt1.setY( pt1.y() - originY );
165  pt2.setX( pt2.x() - originX );
166  pt2.setY( pt2.y() - originY );
167
168  if ( std::isnan( pt1.z() ) || std::isnan( pt2.z() ) )
169  continue;
170
171  nx += ( pt1.y() - pt2.y() ) * ( pt1.z() + pt2.z() );
172  ny += ( pt1.z() - pt2.z() ) * ( pt1.x() + pt2.x() );
173  nz += ( pt1.x() - pt2.x() ) * ( pt1.y() + pt2.y() );
174  }
175
176  QVector3D normal( nx, ny, nz );
177  if ( invertNormal )
178  normal = -normal;
179  normal.normalize();
180  return normal;
181 }
182
183
184 static void _normalVectorToXYVectors( const QVector3D &pNormal, QVector3D &pXVector, QVector3D &pYVector )
185 {
186  // Here we define the two perpendicular vectors that define the local
187  // 2D space on the plane. They will act as axis for which we will
188  // calculate the projection coordinates of a 3D point to the plane.
189  if ( pNormal.z() > 0.001 || pNormal.z() < -0.001 )
190  {
191  pXVector = QVector3D( 1, 0, -pNormal.x() / pNormal.z() );
192  }
193  else if ( pNormal.y() > 0.001 || pNormal.y() < -0.001 )
194  {
195  pXVector = QVector3D( 1, -pNormal.x() / pNormal.y(), 0 );
196  }
197  else
198  {
199  pXVector = QVector3D( -pNormal.y() / pNormal.x(), 1, 0 );
200  }
201  pXVector.normalize();
202  pYVector = QVector3D::normal( pNormal, pXVector );
203 }
204
205
206 static void _ringToPoly2tri( const QgsCurve *ring, std::vector<p2t::Point *> &polyline, QHash<p2t::Point *, float> &zHash )
207 {
209  QgsPoint pt;
210
211  const int pCount = ring->numPoints();
212
213  polyline.reserve( pCount );
214
215  for ( int i = 0; i < pCount - 1; ++i )
216  {
217  ring->pointAt( i, pt, vt );
218  const float x = pt.x();
219  const float y = pt.y();
220  const float z = pt.z();
221
222  const bool found = std::find_if( polyline.begin(), polyline.end(), [x, y]( p2t::Point *&p ) { return *p == p2t::Point( x, y ); } ) != polyline.end();
223
224  if ( found )
225  {
226  continue;
227  }
228
229  p2t::Point *pt2 = new p2t::Point( x, y );
230  polyline.push_back( pt2 );
231  zHash[pt2] = z;
232  }
233 }
234
235
236 inline double _round_coord( double x )
237 {
238  const double exp = 1e10; // round to 10 decimal digits
239  return round( x * exp ) / exp;
240 }
241
242
243 static QgsCurve *_transform_ring_to_new_base( const QgsCurve &curve, const QgsPoint &pt0, const QMatrix4x4 *toNewBase )
244 {
245  int count = curve.numPoints();
246  QVector<QgsPoint> pts;
247  pts.reserve( count );
249  for ( int i = 0; i < count; ++i )
250  {
251  QgsPoint pt;
252  curve.pointAt( i, pt, vt );
253  QgsPoint pt2( QgsWkbTypes::PointZ, pt.x() - pt0.x(), pt.y() - pt0.y(), std::isnan( pt.z() ) ? 0 : pt.z() - pt0.z() );
254  QVector4D v( pt2.x(), pt2.y(), pt2.z(), 0 );
255  if ( toNewBase )
256  v = toNewBase->map( v );
257
258  // we also round coordinates before passing them to poly2tri triangulation in order to fix possible numerical
259  // stability issues. We had crashes with nearly collinear points where one of the points was off by a tiny bit (e.g. by 1e-20).
260  // See TestQgsTessellator::testIssue17745().
261  //
262  // A hint for a similar issue: https://github.com/greenm01/poly2tri/issues/99
263  //
264  // The collinear tests uses epsilon 1e-12. Seems rounding to 12 places you still
265  // can get problems with this test when points are pretty much on a straight line.
266  // I suggest you round to 10 decimals for stability and you can live with that
267  // precision.
268
269  pts << QgsPoint( QgsWkbTypes::PointZ, _round_coord( v.x() ), _round_coord( v.y() ), _round_coord( v.z() ) );
270  }
271  return new QgsLineString( pts );
272 }
273
274
275 static QgsPolygon *_transform_polygon_to_new_base( const QgsPolygon &polygon, const QgsPoint &pt0, const QMatrix4x4 *toNewBase )
276 {
277  QgsPolygon *p = new QgsPolygon;
278  p->setExteriorRing( _transform_ring_to_new_base( *polygon.exteriorRing(), pt0, toNewBase ) );
279  for ( int i = 0; i < polygon.numInteriorRings(); ++i )
280  p->addInteriorRing( _transform_ring_to_new_base( *polygon.interiorRing( i ), pt0, toNewBase ) );
281  return p;
282 }
283
284 static bool _check_intersecting_rings( const QgsPolygon &polygon )
285 {
286  QList<QgsGeometry> geomRings;
287  geomRings << QgsGeometry( polygon.exteriorRing()->clone() );
288  for ( int i = 0; i < polygon.numInteriorRings(); ++i )
289  geomRings << QgsGeometry( polygon.interiorRing( i )->clone() );
290
291  // we need to make sure that the polygon has no rings with self-intersection: that may
292  // crash the tessellator. The original geometry maybe have been valid and the self-intersection
293  // was introduced when transforming to a new base (in a rare case when all points are not in the same plane)
294
295  for ( int i = 0; i < geomRings.count(); ++i )
296  {
297  if ( !geomRings[i].isSimple() )
298  return false;
299  }
300
301  // At this point we assume that input polygons are valid according to the OGC definition.
302  // This means e.g. no duplicate points, polygons are simple (no butterfly shaped polygon with self-intersection),
303  // internal rings are inside exterior rings, rings do not cross each other, no dangles.
304
305  // There is however an issue with polygons where rings touch:
306  // +---+
307  // | |
308  // | +-+-+
309  // | | | |
310  // | +-+ |
311  // | |
312  // +-----+
313  // This is a valid polygon with one exterior and one interior ring that touch at one point,
314  // but poly2tri library does not allow interior rings touch each other or exterior ring.
315  // TODO: Handle the situation better - rather than just detecting the problem, try to fix
316  // it by converting touching rings into one ring.
317
318  if ( polygon.numInteriorRings() > 0 )
319  {
320  for ( int i = 0; i < geomRings.count(); ++i )
321  for ( int j = i + 1; j < geomRings.count(); ++j )
322  {
323  if ( geomRings[i].intersects( geomRings[j] ) )
324  return false;
325  }
326  }
327  return true;
328 }
329
330
332 {
333  double min_d = 1e20;
334  auto it = polygon.vertices_begin();
335
336  if ( it == polygon.vertices_end() )
337  return min_d;
338
339  QgsPoint p0 = *it;
340  ++it;
341  for ( ; it != polygon.vertices_end(); ++it )
342  {
343  QgsPoint p1 = *it;
344  double d = p0.distance( p1 );
345  if ( d < min_d )
346  min_d = d;
347  p0 = p1;
348  }
349  return min_d;
350 }
351
352
353 void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeight )
354 {
355  const QgsCurve *exterior = polygon.exteriorRing();
356
357  const QVector3D pNormal = _calculateNormal( exterior, mOriginX, mOriginY, mInvertNormals );
358  const int pCount = exterior->numPoints();
359
360  if ( pCount == 4 && polygon.numInteriorRings() == 0 )
361  {
362  // polygon is a triangle - write vertices to the output data array without triangulation
363  QgsPoint pt;
365  for ( int i = 0; i < 3; i++ )
366  {
367  exterior->pointAt( i, pt, vt );
368  mData << pt.x() - mOriginX << pt.z() << - pt.y() + mOriginY;
370  mData << pNormal.x() << pNormal.z() << - pNormal.y();
371  }
372
374  {
375  // the same triangle with reversed order of coordinates and inverted normal
376  for ( int i = 2; i >= 0; i-- )
377  {
378  exterior->pointAt( i, pt, vt );
379  mData << pt.x() - mOriginX << pt.z() << - pt.y() + mOriginY;
381  mData << -pNormal.x() << -pNormal.z() << pNormal.y();
382  }
383  }
384  }
385  else
386  {
387  if ( !qgsDoubleNear( pNormal.length(), 1, 0.001 ) )
388  return; // this should not happen - pNormal should be normalized to unit length
389
390  std::unique_ptr<QMatrix4x4> toNewBase, toOldBase;
391  if ( pNormal != QVector3D( 0, 0, 1 ) )
392  {
393  // this is not a horizontal plane - need to reproject the polygon to a new base so that
394  // we can do the triangulation in a plane
395
396  QVector3D pXVector, pYVector;
397  _normalVectorToXYVectors( pNormal, pXVector, pYVector );
398
399  // so now we have three orthogonal unit vectors defining new base
400  // let's build transform matrix. We actually need just a 3x3 matrix,
401  // but Qt does not have good support for it, so using 4x4 matrix instead.
402  toNewBase.reset( new QMatrix4x4(
403  pXVector.x(), pXVector.y(), pXVector.z(), 0,
404  pYVector.x(), pYVector.y(), pYVector.z(), 0,
405  pNormal.x(), pNormal.y(), pNormal.z(), 0,
406  0, 0, 0, 0 ) );
407
408  // our 3x3 matrix is orthogonal, so for inverse we only need to transpose it
409  toOldBase.reset( new QMatrix4x4( toNewBase->transposed() ) );
410  }
411
412  const QgsPoint ptStart( exterior->startPoint() );
413  const QgsPoint pt0( QgsWkbTypes::PointZ, ptStart.x(), ptStart.y(), std::isnan( ptStart.z() ) ? 0 : ptStart.z() );
414
415  // subtract ptFirst from geometry for better numerical stability in triangulation
416  // and apply new 3D vector base if the polygon is not horizontal
417  std::unique_ptr<QgsPolygon> polygonNew( _transform_polygon_to_new_base( polygon, pt0, toNewBase.get() ) );
418
419  if ( _minimum_distance_between_coordinates( *polygonNew ) < 0.001 )
420  {
421  // when the distances between coordinates of input points are very small,
422  // the triangulation likes to crash on numerical errors - when the distances are ~ 1e-5
423  // Assuming that the coordinates should be in a projected CRS, we should be able
424  // to simplify geometries that may cause problems and avoid possible crashes
425  QgsGeometry polygonSimplified = QgsGeometry( polygonNew->clone() ).simplify( 0.001 );
426  if ( polygonSimplified.isNull() )
427  {
428  QgsMessageLog::logMessage( QObject::tr( "geometry simplification failed - skipping" ), QObject::tr( "3D" ) );
429  return;
430  }
431  const QgsPolygon *polygonSimplifiedData = qgsgeometry_cast<const QgsPolygon *>( polygonSimplified.constGet() );
432  if ( _minimum_distance_between_coordinates( *polygonSimplifiedData ) < 0.001 )
433  {
434  // Failed to fix that. It could be a really tiny geometry... or maybe they gave us
435  // geometry in unprojected lat/lon coordinates
436  QgsMessageLog::logMessage( QObject::tr( "geometry's coordinates are too close to each other and simplification failed - skipping" ), QObject::tr( "3D" ) );
437  return;
438  }
439  else
440  {
441  polygonNew.reset( polygonSimplifiedData->clone() );
442  }
443  }
444
445  if ( !_check_intersecting_rings( *polygonNew ) )
446  {
447  // skip the polygon - it would cause a crash inside poly2tri library
448  QgsMessageLog::logMessage( QObject::tr( "polygon rings self-intersect or intersect each other - skipping" ), QObject::tr( "3D" ) );
449  return;
450  }
451
452  QList< std::vector<p2t::Point *> > polylinesToDelete;
453  QHash<p2t::Point *, float> z;
454
455  // polygon exterior
456  std::vector<p2t::Point *> polyline;
457  _ringToPoly2tri( polygonNew->exteriorRing(), polyline, z );
458  polylinesToDelete << polyline;
459
460  std::unique_ptr<p2t::CDT> cdt( new p2t::CDT( polyline ) );
461
462  // polygon holes
463  for ( int i = 0; i < polygonNew->numInteriorRings(); ++i )
464  {
465  std::vector<p2t::Point *> holePolyline;
466  const QgsCurve *hole = polygonNew->interiorRing( i );
467
468  _ringToPoly2tri( hole, holePolyline, z );
469
471  polylinesToDelete << holePolyline;
472  }
473
474  // run triangulation and write vertices to the output data array
475  try
476  {
477  cdt->Triangulate();
478
479  std::vector<p2t::Triangle *> triangles = cdt->GetTriangles();
480
481  for ( size_t i = 0; i < triangles.size(); ++i )
482  {
483  p2t::Triangle *t = triangles[i];
484  for ( int j = 0; j < 3; ++j )
485  {
486  p2t::Point *p = t->GetPoint( j );
487  QVector4D pt( p->x, p->y, z[p], 0 );
488  if ( toOldBase )
489  pt = *toOldBase * pt;
490  const double fx = pt.x() - mOriginX + pt0.x();
491  const double fy = pt.y() - mOriginY + pt0.y();
492  const double fz = pt.z() + extrusionHeight + pt0.z();
493  mData << fx << fz << -fy;
495  mData << pNormal.x() << pNormal.z() << - pNormal.y();
496  }
497
499  {
500  // the same triangle with reversed order of coordinates and inverted normal
501  for ( int j = 2; j >= 0; --j )
502  {
503  p2t::Point *p = t->GetPoint( j );
504  QVector4D pt( p->x, p->y, z[p], 0 );
505  if ( toOldBase )
506  pt = *toOldBase * pt;
507  const double fx = pt.x() - mOriginX + pt0.x();
508  const double fy = pt.y() - mOriginY + pt0.y();
509  const double fz = pt.z() + extrusionHeight + pt0.z();
510  mData << fx << fz << -fy;
512  mData << -pNormal.x() << -pNormal.z() << pNormal.y();
513  }
514  }
515  }
516  }
517  catch ( ... )
518  {
519  QgsMessageLog::logMessage( QObject::tr( "Triangulation failed. Skipping polygonâ€¦" ), QObject::tr( "3D" ) );
520  }
521
522  for ( int i = 0; i < polylinesToDelete.count(); ++i )
523  qDeleteAll( polylinesToDelete[i] );
524  }
525
526  // add walls if extrusion is enabled
527  if ( extrusionHeight != 0 )
528  {
529  _makeWalls( *exterior, false, extrusionHeight, mData, mAddNormals, mOriginX, mOriginY );
530
531  for ( int i = 0; i < polygon.numInteriorRings(); ++i )
532  _makeWalls( *polygon.interiorRing( i ), true, extrusionHeight, mData, mAddNormals, mOriginX, mOriginY );
533  }
534 }
535
536 QgsPoint getPointFromData( QVector< float >::const_iterator &it )
537 {
538  // tessellator geometry is x, z, -y
539  double x = *it;
540  ++it;
541  double z = *it;
542  ++it;
543  double y = -( *it );
544  ++it;
545  return QgsPoint( x, y, z );
546 }
547
549 {
550  return mData.size() / ( mAddNormals ? 6 : 3 );
551 }
552
553 std::unique_ptr<QgsMultiPolygon> QgsTessellator::asMultiPolygon() const
554 {
555  std::unique_ptr< QgsMultiPolygon > mp = qgis::make_unique< QgsMultiPolygon >();
556  const QVector<float> data = mData;
557  for ( auto it = data.constBegin(); it != data.constEnd(); )
558  {
559  QgsPoint p1 = getPointFromData( it );
560  QgsPoint p2 = getPointFromData( it );
561  QgsPoint p3 = getPointFromData( it );
562  mp->addGeometry( new QgsTriangle( p1, p2, p3 ) );
563  }
564  return mp;
565 }
double y
Definition: qgspoint.h:42
double distance(double x, double y) const
Returns the distance between this point and a specified x, y coordinate.
Definition: qgspoint.h:276
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:265
const QgsCurve * interiorRing(int i) const
Retrieves an interior ring from the curve polygon.
A geometry is the spatial representation of a feature.
Definition: qgsgeometry.h:111
int dataVerticesCount() const
Returns the number of vertices stored in the output data array.
virtual bool pointAt(int node, QgsPoint &point, QgsVertexId::VertexType &type) const =0
Returns the point and vertex id of a point within the curve.
std::unique_ptr< QgsMultiPolygon > asMultiPolygon() const
Returns the triangulation as a multipolygon geometry.
QgsPoint getPointFromData(QVector< float >::const_iterator &it)
Triangle geometry type.
Definition: qgstriangle.h:33
static bool hasZ(Type type)
Tests whether a WKB type contains the z-dimension.
Definition: qgswkbtypes.h:771
Adds an interior ring to the geometry (takes ownership)
Definition: qgspolygon.cpp:148
int numInteriorRings() const
Returns the number of interior rings contained with the curve polygon.
double _round_coord(double x)
void addPolygon(const QgsPolygon &polygon, float extrusionHeight)
Tessellates a triangle and adds its vertex entries to the output data array.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
vertex_iterator vertices_end() const
Returns STL-style iterator pointing to the imaginary vertex after the last vertex of the geometry...
T qgsgeometry_cast(const QgsAbstractGeometry *geom)
Creates tessellator with a specified origin point of the world (in map coordinates) ...
Abstract base class for curved geometry type.
Definition: qgscurve.h:35
QgsWkbTypes::Type wkbType() const
Returns the WKB type of the geometry.
Point geometry type, with support for z-dimension and m-values.
Definition: qgspoint.h:37
double _minimum_distance_between_coordinates(const QgsPolygon &polygon)
void setX(double x)
Sets the point&#39;s x-coordinate.
Definition: qgspoint.h:213
const QgsAbstractGeometry * constGet() const
Returns a non-modifiable (const) reference to the underlying abstract geometry primitive.
void setY(double y)
Sets the point&#39;s y-coordinate.
Definition: qgspoint.h:224
void setExteriorRing(QgsCurve *ring) override
Sets the exterior ring of the polygon.
Definition: qgspolygon.cpp:179
QgsCurve * clone() const override=0
Clones the geometry by performing a deep copy.
Line string geometry type, with support for z-dimension and m-values.
Definition: qgslinestring.h:43
QgsPolygon * clone() const override
Clones the geometry by performing a deep copy.
Definition: qgspolygon.cpp:42
double z
Definition: qgspoint.h:43
vertex_iterator vertices_begin() const
Returns STL-style iterator pointing to the first vertex of the geometry.
virtual QgsPoint startPoint() const =0
Returns the starting point of the curve.
Polygon geometry type.
Definition: qgspolygon.h:31
const QgsCurve * exteriorRing() const
Returns the curve polygon&#39;s exterior ring.
virtual int numPoints() const =0
Returns the number of points in the curve.
double x
Definition: qgspoint.h:41