static bool _check_intersecting_rings( const QgsPolygon &polygon ) { // At this point we assume that input polygons are valid according to the OGC definition. // This means e.g. no duplicate points, polygons are simple (no butterfly shaped polygon with self-intersection), // internal rings are inside exterior rings, rings do not cross each other, no dangles. // There is however an issue with polygons where rings touch: // +---+ // | | // | +-+-+ // | | | | // | +-+ | // | | // +-----+ // This is a valid polygon with one exterior and one interior ring that touch at one point, // but poly2tri library does not allow interior rings touch each other or exterior ring. // TODO: Handle the situation better - rather than just detecting the problem, try to fix // it by converting touching rings into one ring. if ( polygon.numInteriorRings() > 0 ) { QList<QgsGeometry> geomRings; geomRings << QgsGeometry( polygon.exteriorRing()->clone() ); for ( int i = 0; i < polygon.numInteriorRings(); ++i ) geomRings << QgsGeometry( polygon.interiorRing( i )->clone() ); for ( int i = 0; i < geomRings.count(); ++i ) for ( int j = i + 1; j < geomRings.count(); ++j ) { if ( geomRings[i].intersects( geomRings[j] ) ) return false; } } return true; }
static bool _check_intersecting_rings( const QgsPolygon &polygon ) { QList<QgsGeometry> geomRings; geomRings << QgsGeometry( polygon.exteriorRing()->clone() ); for ( int i = 0; i < polygon.numInteriorRings(); ++i ) geomRings << QgsGeometry( polygon.interiorRing( i )->clone() ); // we need to make sure that the polygon has no rings with self-intersection: that may // crash the tessellator. The original geometry maybe have been valid and the self-intersection // was introduced when transforming to a new base (in a rare case when all points are not in the same plane) for ( int i = 0; i < geomRings.count(); ++i ) { if ( !geomRings[i].isSimple() ) return false; } // At this point we assume that input polygons are valid according to the OGC definition. // This means e.g. no duplicate points, polygons are simple (no butterfly shaped polygon with self-intersection), // internal rings are inside exterior rings, rings do not cross each other, no dangles. // There is however an issue with polygons where rings touch: // +---+ // | | // | +-+-+ // | | | | // | +-+ | // | | // +-----+ // This is a valid polygon with one exterior and one interior ring that touch at one point, // but poly2tri library does not allow interior rings touch each other or exterior ring. // TODO: Handle the situation better - rather than just detecting the problem, try to fix // it by converting touching rings into one ring. if ( polygon.numInteriorRings() > 0 ) { for ( int i = 0; i < geomRings.count(); ++i ) for ( int j = i + 1; j < geomRings.count(); ++j ) { if ( geomRings[i].intersects( geomRings[j] ) ) return false; } } return true; }
void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeight ) { if ( _minimum_distance_between_coordinates( polygon ) < 0.001 ) { // when the distances between coordinates of input points are very small, // the triangulation likes to crash on numerical errors - when the distances are ~ 1e-5 // Assuming that the coordinates should be in a projected CRS, we should be able // to simplify geometries that may cause problems and avoid possible crashes QgsGeometry polygonSimplified = QgsGeometry( polygon.clone() ).simplify( 0.001 ); const QgsPolygon *polygonSimplifiedData = qgsgeometry_cast<const QgsPolygon *>( polygonSimplified.constGet() ); if ( _minimum_distance_between_coordinates( *polygonSimplifiedData ) < 0.001 ) { // Failed to fix that. It could be a really tiny geometry... or maybe they gave us // geometry in unprojected lat/lon coordinates QgsMessageLog::logMessage( "geometry's coordinates are too close to each other and simplification failed - skipping", "3D" ); } else { addPolygon( *polygonSimplifiedData, extrusionHeight ); } return; } if ( !_check_intersecting_rings( polygon ) ) { // skip the polygon - it would cause a crash inside poly2tri library QgsMessageLog::logMessage( "polygon rings intersect each other - skipping", "3D" ); return; } const QgsCurve *exterior = polygon.exteriorRing(); QList< std::vector<p2t::Point *> > polylinesToDelete; QHash<p2t::Point *, float> z; std::vector<p2t::Point *> polyline; const QVector3D pNormal = _calculateNormal( exterior, mOriginX, mOriginY ); const int pCount = exterior->numPoints(); // Polygon is a triangle if ( pCount == 4 ) { QgsPoint pt; QgsVertexId::VertexType vt; for ( int i = 0; i < 3; i++ ) { exterior->pointAt( i, pt, vt ); mData << pt.x() - mOriginX << pt.z() << - pt.y() + mOriginY; if ( mAddNormals ) mData << pNormal.x() << pNormal.z() << - pNormal.y(); } } else { if ( !qgsDoubleNear( pNormal.length(), 1, 0.001 ) ) return; // this should not happen - pNormal should be normalized to unit length QVector3D pXVector, pYVector; _normalVectorToXYVectors( pNormal, pXVector, pYVector ); // so now we have three orthogonal unit vectors defining new base // let's build transform matrix. We actually need just a 3x3 matrix, // but Qt does not have good support for it, so using 4x4 matrix instead. QMatrix4x4 toNewBase( pXVector.x(), pXVector.y(), pXVector.z(), 0, pYVector.x(), pYVector.y(), pYVector.z(), 0, pNormal.x(), pNormal.y(), pNormal.z(), 0, 0, 0, 0, 0 ); // our 3x3 matrix is orthogonal, so for inverse we only need to transpose it QMatrix4x4 toOldBase = toNewBase.transposed(); const QgsPoint ptFirst( exterior->startPoint() ); _ringToPoly2tri( exterior, ptFirst, toNewBase, polyline, z ); polylinesToDelete << polyline; // TODO: robustness (no nearly duplicate points, invalid geometries ...) double x0 = ptFirst.x(), y0 = ptFirst.y(), z0 = ( std::isnan( ptFirst.z() ) ? 0 : ptFirst.z() ); if ( polyline.size() == 3 && polygon.numInteriorRings() == 0 ) { for ( std::vector<p2t::Point *>::iterator it = polyline.begin(); it != polyline.end(); it++ ) { p2t::Point *p = *it; QVector4D ptInNewBase( p->x, p->y, z[p], 0 ); QVector4D nPoint = toOldBase * ptInNewBase; const double fx = nPoint.x() - mOriginX + x0; const double fy = nPoint.y() - mOriginY + y0; const double fz = nPoint.z() + extrusionHeight + z0; mData << fx << fz << -fy; if ( mAddNormals ) mData << pNormal.x() << pNormal.z() << - pNormal.y(); } } else if ( polyline.size() >= 3 ) { p2t::CDT *cdt = new p2t::CDT( polyline ); // polygon holes for ( int i = 0; i < polygon.numInteriorRings(); ++i ) { std::vector<p2t::Point *> holePolyline; const QgsCurve *hole = polygon.interiorRing( i ); _ringToPoly2tri( hole, ptFirst, toNewBase, holePolyline, z ); cdt->AddHole( holePolyline ); polylinesToDelete << holePolyline; } try { cdt->Triangulate(); std::vector<p2t::Triangle *> triangles = cdt->GetTriangles(); for ( size_t i = 0; i < triangles.size(); ++i ) { p2t::Triangle *t = triangles[i]; for ( int j = 0; j < 3; ++j ) { p2t::Point *p = t->GetPoint( j ); QVector4D ptInNewBase( p->x, p->y, z[p], 0 ); QVector4D nPoint = toOldBase * ptInNewBase; const double fx = nPoint.x() - mOriginX + x0; const double fy = nPoint.y() - mOriginY + y0; const double fz = nPoint.z() + extrusionHeight + z0; mData << fx << fz << -fy; if ( mAddNormals ) mData << pNormal.x() << pNormal.z() << - pNormal.y(); } } } catch ( ... ) { QgsMessageLog::logMessage( "Triangulation failed. Skipping polygon...", "3D" ); } delete cdt; } for ( int i = 0; i < polylinesToDelete.count(); ++i ) qDeleteAll( polylinesToDelete[i] ); } // add walls if extrusion is enabled if ( extrusionHeight != 0 ) { _makeWalls( *exterior, false, extrusionHeight, mData, mAddNormals, mOriginX, mOriginY ); for ( int i = 0; i < polygon.numInteriorRings(); ++i ) _makeWalls( *polygon.interiorRing( i ), true, extrusionHeight, mData, mAddNormals, mOriginX, mOriginY ); } }
void QgsTessellator::addPolygon( const QgsPolygon &polygon, float extrusionHeight ) { const QgsCurve *exterior = polygon.exteriorRing(); const QVector3D pNormal = _calculateNormal( exterior, mOriginX, mOriginY, mInvertNormals ); const int pCount = exterior->numPoints(); if ( pCount == 4 && polygon.numInteriorRings() == 0 ) { // polygon is a triangle - write vertices to the output data array without triangulation QgsPoint pt; QgsVertexId::VertexType vt; for ( int i = 0; i < 3; i++ ) { exterior->pointAt( i, pt, vt ); mData << pt.x() - mOriginX << pt.z() << - pt.y() + mOriginY; if ( mAddNormals ) mData << pNormal.x() << pNormal.z() << - pNormal.y(); } if ( mAddBackFaces ) { // the same triangle with reversed order of coordinates and inverted normal for ( int i = 2; i >= 0; i-- ) { exterior->pointAt( i, pt, vt ); mData << pt.x() - mOriginX << pt.z() << - pt.y() + mOriginY; if ( mAddNormals ) mData << -pNormal.x() << -pNormal.z() << pNormal.y(); } } } else { if ( !qgsDoubleNear( pNormal.length(), 1, 0.001 ) ) return; // this should not happen - pNormal should be normalized to unit length std::unique_ptr<QMatrix4x4> toNewBase, toOldBase; if ( pNormal != QVector3D( 0, 0, 1 ) ) { // this is not a horizontal plane - need to reproject the polygon to a new base so that // we can do the triangulation in a plane QVector3D pXVector, pYVector; _normalVectorToXYVectors( pNormal, pXVector, pYVector ); // so now we have three orthogonal unit vectors defining new base // let's build transform matrix. We actually need just a 3x3 matrix, // but Qt does not have good support for it, so using 4x4 matrix instead. toNewBase.reset( new QMatrix4x4( pXVector.x(), pXVector.y(), pXVector.z(), 0, pYVector.x(), pYVector.y(), pYVector.z(), 0, pNormal.x(), pNormal.y(), pNormal.z(), 0, 0, 0, 0, 0 ) ); // our 3x3 matrix is orthogonal, so for inverse we only need to transpose it toOldBase.reset( new QMatrix4x4( toNewBase->transposed() ) ); } const QgsPoint ptStart( exterior->startPoint() ); const QgsPoint pt0( QgsWkbTypes::PointZ, ptStart.x(), ptStart.y(), std::isnan( ptStart.z() ) ? 0 : ptStart.z() ); // subtract ptFirst from geometry for better numerical stability in triangulation // and apply new 3D vector base if the polygon is not horizontal std::unique_ptr<QgsPolygon> polygonNew( _transform_polygon_to_new_base( polygon, pt0, toNewBase.get() ) ); if ( _minimum_distance_between_coordinates( *polygonNew ) < 0.001 ) { // when the distances between coordinates of input points are very small, // the triangulation likes to crash on numerical errors - when the distances are ~ 1e-5 // Assuming that the coordinates should be in a projected CRS, we should be able // to simplify geometries that may cause problems and avoid possible crashes QgsGeometry polygonSimplified = QgsGeometry( polygonNew->clone() ).simplify( 0.001 ); const QgsPolygon *polygonSimplifiedData = qgsgeometry_cast<const QgsPolygon *>( polygonSimplified.constGet() ); if ( _minimum_distance_between_coordinates( *polygonSimplifiedData ) < 0.001 ) { // Failed to fix that. It could be a really tiny geometry... or maybe they gave us // geometry in unprojected lat/lon coordinates QgsMessageLog::logMessage( QObject::tr( "geometry's coordinates are too close to each other and simplification failed - skipping" ), QObject::tr( "3D" ) ); return; } else { polygonNew.reset( polygonSimplifiedData->clone() ); } } if ( !_check_intersecting_rings( *polygonNew.get() ) ) { // skip the polygon - it would cause a crash inside poly2tri library QgsMessageLog::logMessage( QObject::tr( "polygon rings self-intersect or intersect each other - skipping" ), QObject::tr( "3D" ) ); return; } QList< std::vector<p2t::Point *> > polylinesToDelete; QHash<p2t::Point *, float> z; // polygon exterior std::vector<p2t::Point *> polyline; _ringToPoly2tri( polygonNew->exteriorRing(), polyline, z ); polylinesToDelete << polyline; std::unique_ptr<p2t::CDT> cdt( new p2t::CDT( polyline ) ); // polygon holes for ( int i = 0; i < polygonNew->numInteriorRings(); ++i ) { std::vector<p2t::Point *> holePolyline; const QgsCurve *hole = polygonNew->interiorRing( i ); _ringToPoly2tri( hole, holePolyline, z ); cdt->AddHole( holePolyline ); polylinesToDelete << holePolyline; } // run triangulation and write vertices to the output data array try { cdt->Triangulate(); std::vector<p2t::Triangle *> triangles = cdt->GetTriangles(); for ( size_t i = 0; i < triangles.size(); ++i ) { p2t::Triangle *t = triangles[i]; for ( int j = 0; j < 3; ++j ) { p2t::Point *p = t->GetPoint( j ); QVector4D pt( p->x, p->y, z[p], 0 ); if ( toOldBase ) pt = *toOldBase * pt; const double fx = pt.x() - mOriginX + pt0.x(); const double fy = pt.y() - mOriginY + pt0.y(); const double fz = pt.z() + extrusionHeight + pt0.z(); mData << fx << fz << -fy; if ( mAddNormals ) mData << pNormal.x() << pNormal.z() << - pNormal.y(); } if ( mAddBackFaces ) { // the same triangle with reversed order of coordinates and inverted normal for ( int j = 2; j >= 0; --j ) { p2t::Point *p = t->GetPoint( j ); QVector4D pt( p->x, p->y, z[p], 0 ); if ( toOldBase ) pt = *toOldBase * pt; const double fx = pt.x() - mOriginX + pt0.x(); const double fy = pt.y() - mOriginY + pt0.y(); const double fz = pt.z() + extrusionHeight + pt0.z(); mData << fx << fz << -fy; if ( mAddNormals ) mData << -pNormal.x() << -pNormal.z() << pNormal.y(); } } } } catch ( ... ) { QgsMessageLog::logMessage( QObject::tr( "Triangulation failed. Skipping polygon…" ), QObject::tr( "3D" ) ); } for ( int i = 0; i < polylinesToDelete.count(); ++i ) qDeleteAll( polylinesToDelete[i] ); } // add walls if extrusion is enabled if ( extrusionHeight != 0 ) { _makeWalls( *exterior, false, extrusionHeight, mData, mAddNormals, mOriginX, mOriginY ); for ( int i = 0; i < polygon.numInteriorRings(); ++i ) _makeWalls( *polygon.interiorRing( i ), true, extrusionHeight, mData, mAddNormals, mOriginX, mOriginY ); } }