diff --git a/python/PyQt6/core/auto_additions/qgsgeometry.py b/python/PyQt6/core/auto_additions/qgsgeometry.py index c8dc6afb58e6..60f8167f6915 100644 --- a/python/PyQt6/core/auto_additions/qgsgeometry.py +++ b/python/PyQt6/core/auto_additions/qgsgeometry.py @@ -12,6 +12,7 @@ QgsGeometry.fromRect = staticmethod(QgsGeometry.fromRect) QgsGeometry.fromBox3D = staticmethod(QgsGeometry.fromBox3D) QgsGeometry.collectGeometry = staticmethod(QgsGeometry.collectGeometry) + QgsGeometry.collectTinPatches = staticmethod(QgsGeometry.collectTinPatches) QgsGeometry.createWedgeBuffer = staticmethod(QgsGeometry.createWedgeBuffer) QgsGeometry.createWedgeBufferFromAngles = staticmethod(QgsGeometry.createWedgeBufferFromAngles) QgsGeometry.unaryUnion = staticmethod(QgsGeometry.unaryUnion) diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in index cfa76dfc0e9a..036dd9352c38 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometry.sip.in @@ -41,7 +41,6 @@ Encapsulates parameters under which a geometry operation is performed. #include "qgsgeometry.h" %End public: - double gridSize() const; %Docstring Returns the grid size which will be used to snap vertices of a geometry. @@ -105,7 +104,6 @@ as the point, line, polygon, curve or other geometry subclasses. static const QMetaObject staticMetaObject; public: - QgsGeometry() /HoldGIL/; QgsGeometry( const QgsGeometry & ); @@ -290,8 +288,31 @@ surface geometry. Creates a new multipart geometry from a list of QgsGeometry objects %End - static QgsGeometry createWedgeBuffer( const QgsPoint ¢er, double azimuth, double angularWidth, - double outerRadius, double innerRadius = 0 ); + static QgsGeometry collectTinPatches( const QVector &geometries ); +%Docstring +Collects all patches from a list of TIN or Triangle geometries into a +single TIN geometry. + +This method iterates through the input ``geometries`` and extracts all +triangle patches, combining them into a single +:py:class:`QgsTriangulatedSurface`. Input geometries can be either +:py:class:`QgsTriangulatedSurface` (TIN) or :py:class:`QgsTriangle` +objects. Other geometry types are ignored. + +The resulting TIN will preserve Z and M values if present in the first +valid input geometry. + +:param geometries: list of input geometries (should be TIN or Triangle + types) + +:return: a QgsGeometry containing a QgsTriangulatedSurface with all + collected patches, or a null geometry if no valid TIN/Triangle + geometries were found + +.. versionadded:: 4.0 +%End + + static QgsGeometry createWedgeBuffer( const QgsPoint ¢er, double azimuth, double angularWidth, double outerRadius, double innerRadius = 0 ); %Docstring Creates a wedge shaped buffer from a ``center`` point. @@ -310,8 +331,7 @@ Polygon geometry. .. versionadded:: 3.2 %End - static QgsGeometry createWedgeBufferFromAngles( const QgsPoint ¢er, double startAngle, double endAngle, - double outerRadius, double innerRadius = 0 ); + static QgsGeometry createWedgeBufferFromAngles( const QgsPoint ¢er, double startAngle, double endAngle, double outerRadius, double innerRadius = 0 ); %Docstring Creates a wedge shaped buffer from a ``center`` point. @@ -1111,8 +1131,7 @@ Example: if ( PyList_Check( a0 ) && PyList_GET_SIZE( a0 ) ) { PyObject *p0 = PyList_GetItem( a0, 0 ); - if ( sipCanConvertToType( p0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( p0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QVector newGeometries; QVector topologyTestPoints; @@ -1134,8 +1153,7 @@ Example: sipReleaseType( splitLine, sipType_QVector_0100QgsPointXY, state ); } - else if ( sipCanConvertToType( p0, sipType_QgsPoint, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPoint, SIP_NOT_NONE ) ) + else if ( sipCanConvertToType( p0, sipType_QgsPoint, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPoint, SIP_NOT_NONE ) ) { QVector newGeometries; QVector topologyTestPoints; @@ -1170,7 +1188,7 @@ Example: } %End - Qgis::GeometryOperationResult splitGeometry( const QgsCurve *curve, QVector &newGeometries /Out/, bool preserveCircular, bool topological, QgsPointSequence &topologyTestPoints /Out/, bool splitFeature = true ); + Qgis::GeometryOperationResult splitGeometry( const QgsCurve *curve, QVector &newGeometries /Out/, bool preserveCircular, bool topological, QgsPointSequence &topologyTestPoints /Out/, bool splitFeature = true ); %Docstring Splits this geometry according to a given curve. @@ -1391,11 +1409,7 @@ will be generated. .. versionadded:: 3.24 %End - QgsGeometry applyDashPattern( const QVector< double > &pattern, - Qgis::DashPatternLineEndingRule startRule = Qgis::DashPatternLineEndingRule::NoRule, - Qgis::DashPatternLineEndingRule endRule = Qgis::DashPatternLineEndingRule::NoRule, - Qgis::DashPatternSizeAdjustment adjustment = Qgis::DashPatternSizeAdjustment::ScaleBothDashAndGap, - double patternOffset = 0 ) const; + QgsGeometry applyDashPattern( const QVector< double > &pattern, Qgis::DashPatternLineEndingRule startRule = Qgis::DashPatternLineEndingRule::NoRule, Qgis::DashPatternLineEndingRule endRule = Qgis::DashPatternLineEndingRule::NoRule, Qgis::DashPatternSizeAdjustment adjustment = Qgis::DashPatternSizeAdjustment::ScaleBothDashAndGap, double patternOffset = 0 ) const; %Docstring Applies a dash pattern to a geometry, returning a MultiLineString geometry which is the input geometry stroked along each line/ring with @@ -1629,9 +1643,7 @@ Returns an offset line at a given distance and side from an input line. (JoinStyleMiter only) %End - QgsGeometry singleSidedBuffer( double distance, int segments, Qgis::BufferSide side, - Qgis::JoinStyle joinStyle = Qgis::JoinStyle::Round, - double miterLimit = 2.0 ) const; + QgsGeometry singleSidedBuffer( double distance, int segments, Qgis::BufferSide side, Qgis::JoinStyle joinStyle = Qgis::JoinStyle::Round, double miterLimit = 2.0 ) const; %Docstring Returns a single sided buffer for a (multi)line geometry. The buffer is only applied to one side of the line. @@ -2358,6 +2370,8 @@ the specified type. E.g. - curved geometries will be segmented if ``type`` is non-curved. - multi geometries will be converted to a list of single geometries - single geometries will be upgraded to multi geometries +- PolyhedralSurface/TIN will be converted to multi polygon +- multipolygon, polygon and triangle will be converted to TIN - z or m values will be added or dropped as required. Since QGIS 3.24, the parameters ``defaultZ`` and ``defaultM`` control @@ -2375,6 +2389,12 @@ geometries to points. By default duplicated nodes are ignored. to coerce geometries to the desired ``type``. It also correctly maintains curves and z/m values wherever appropriate. +.. note:: + + If an error occurs during conversion (e.g., attempting to convert a polygon with + non-triangular vertices to a Triangle or TIN geometry), an empty vector will be returned and the + error message can be retrieved by calling :py:func:`~QgsGeometry.lastError` on the returned geometry. + .. versionadded:: 3.14 %End @@ -3093,18 +3113,14 @@ should match. int state1; int sipIsErr = 0; - if ( PyList_Check( a0 ) && PyList_Check( a1 ) && - PyList_GET_SIZE( a0 ) && PyList_GET_SIZE( a1 ) ) + if ( PyList_Check( a0 ) && PyList_Check( a1 ) && PyList_GET_SIZE( a0 ) && PyList_GET_SIZE( a1 ) ) { PyObject *o0 = PyList_GetItem( a0, 0 ); PyObject *o1 = PyList_GetItem( a1, 0 ); if ( o0 && o1 ) { // compare polyline - polyline - if ( sipCanConvertToType( o0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( o1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( o0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( o1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsPolylineXY *p0; QgsPolylineXY *p1; @@ -3117,18 +3133,14 @@ should match. sipReleaseType( p0, sipType_QVector_0100QgsPointXY, state0 ); sipReleaseType( p1, sipType_QVector_0100QgsPointXY, state1 ); } - else if ( PyList_Check( o0 ) && PyList_Check( o1 ) && - PyList_GET_SIZE( o0 ) && PyList_GET_SIZE( o1 ) ) + else if ( PyList_Check( o0 ) && PyList_Check( o1 ) && PyList_GET_SIZE( o0 ) && PyList_GET_SIZE( o1 ) ) { PyObject *oo0 = PyList_GetItem( o0, 0 ); PyObject *oo1 = PyList_GetItem( o1, 0 ); if ( oo0 && oo1 ) { // compare polygon - polygon - if ( sipCanConvertToType( oo0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( oo1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( oo0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( oo1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsPolygonXY *p0; QgsPolygonXY *p1; @@ -3141,18 +3153,14 @@ should match. sipReleaseType( p0, sipType_QVector_0600QVector_0100QgsPointXY, state0 ); sipReleaseType( p1, sipType_QVector_0600QVector_0100QgsPointXY, state1 ); } - else if ( PyList_Check( oo0 ) && PyList_Check( oo1 ) && - PyList_GET_SIZE( oo0 ) && PyList_GET_SIZE( oo1 ) ) + else if ( PyList_Check( oo0 ) && PyList_Check( oo1 ) && PyList_GET_SIZE( oo0 ) && PyList_GET_SIZE( oo1 ) ) { PyObject *ooo0 = PyList_GetItem( oo0, 0 ); PyObject *ooo1 = PyList_GetItem( oo1, 0 ); if ( ooo0 && ooo1 ) { // compare multipolygon - multipolygon - if ( sipCanConvertToType( ooo0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( ooo1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( ooo0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( ooo1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsMultiPolygonXY *p0; QgsMultiPolygonXY *p1; @@ -3174,8 +3182,7 @@ should match. } %End - QgsGeometry smooth( unsigned int iterations = 1, double offset = 0.25, - double minimumDistance = -1.0, double maxAngle = 180.0 ) const; + QgsGeometry smooth( unsigned int iterations = 1, double offset = 0.25, double minimumDistance = -1.0, double maxAngle = 180.0 ) const; %Docstring Smooths a geometry by rounding off corners using the Chaikin algorithm. This operation roughly doubles the number of vertices in a geometry. diff --git a/python/core/auto_additions/qgsgeometry.py b/python/core/auto_additions/qgsgeometry.py index c8dc6afb58e6..60f8167f6915 100644 --- a/python/core/auto_additions/qgsgeometry.py +++ b/python/core/auto_additions/qgsgeometry.py @@ -12,6 +12,7 @@ QgsGeometry.fromRect = staticmethod(QgsGeometry.fromRect) QgsGeometry.fromBox3D = staticmethod(QgsGeometry.fromBox3D) QgsGeometry.collectGeometry = staticmethod(QgsGeometry.collectGeometry) + QgsGeometry.collectTinPatches = staticmethod(QgsGeometry.collectTinPatches) QgsGeometry.createWedgeBuffer = staticmethod(QgsGeometry.createWedgeBuffer) QgsGeometry.createWedgeBufferFromAngles = staticmethod(QgsGeometry.createWedgeBufferFromAngles) QgsGeometry.unaryUnion = staticmethod(QgsGeometry.unaryUnion) diff --git a/python/core/auto_generated/geometry/qgsgeometry.sip.in b/python/core/auto_generated/geometry/qgsgeometry.sip.in index f24e1079922a..ce323c5aa897 100644 --- a/python/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometry.sip.in @@ -41,7 +41,6 @@ Encapsulates parameters under which a geometry operation is performed. #include "qgsgeometry.h" %End public: - double gridSize() const; %Docstring Returns the grid size which will be used to snap vertices of a geometry. @@ -105,7 +104,6 @@ as the point, line, polygon, curve or other geometry subclasses. static const QMetaObject staticMetaObject; public: - QgsGeometry() /HoldGIL/; QgsGeometry( const QgsGeometry & ); @@ -290,8 +288,31 @@ surface geometry. Creates a new multipart geometry from a list of QgsGeometry objects %End - static QgsGeometry createWedgeBuffer( const QgsPoint ¢er, double azimuth, double angularWidth, - double outerRadius, double innerRadius = 0 ); + static QgsGeometry collectTinPatches( const QVector &geometries ); +%Docstring +Collects all patches from a list of TIN or Triangle geometries into a +single TIN geometry. + +This method iterates through the input ``geometries`` and extracts all +triangle patches, combining them into a single +:py:class:`QgsTriangulatedSurface`. Input geometries can be either +:py:class:`QgsTriangulatedSurface` (TIN) or :py:class:`QgsTriangle` +objects. Other geometry types are ignored. + +The resulting TIN will preserve Z and M values if present in the first +valid input geometry. + +:param geometries: list of input geometries (should be TIN or Triangle + types) + +:return: a QgsGeometry containing a QgsTriangulatedSurface with all + collected patches, or a null geometry if no valid TIN/Triangle + geometries were found + +.. versionadded:: 4.0 +%End + + static QgsGeometry createWedgeBuffer( const QgsPoint ¢er, double azimuth, double angularWidth, double outerRadius, double innerRadius = 0 ); %Docstring Creates a wedge shaped buffer from a ``center`` point. @@ -310,8 +331,7 @@ Polygon geometry. .. versionadded:: 3.2 %End - static QgsGeometry createWedgeBufferFromAngles( const QgsPoint ¢er, double startAngle, double endAngle, - double outerRadius, double innerRadius = 0 ); + static QgsGeometry createWedgeBufferFromAngles( const QgsPoint ¢er, double startAngle, double endAngle, double outerRadius, double innerRadius = 0 ); %Docstring Creates a wedge shaped buffer from a ``center`` point. @@ -1111,8 +1131,7 @@ Example: if ( PyList_Check( a0 ) && PyList_GET_SIZE( a0 ) ) { PyObject *p0 = PyList_GetItem( a0, 0 ); - if ( sipCanConvertToType( p0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( p0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QVector newGeometries; QVector topologyTestPoints; @@ -1134,8 +1153,7 @@ Example: sipReleaseType( splitLine, sipType_QVector_0100QgsPointXY, state ); } - else if ( sipCanConvertToType( p0, sipType_QgsPoint, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPoint, SIP_NOT_NONE ) ) + else if ( sipCanConvertToType( p0, sipType_QgsPoint, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPoint, SIP_NOT_NONE ) ) { QVector newGeometries; QVector topologyTestPoints; @@ -1170,7 +1188,7 @@ Example: } %End - Qgis::GeometryOperationResult splitGeometry( const QgsCurve *curve, QVector &newGeometries /Out/, bool preserveCircular, bool topological, QgsPointSequence &topologyTestPoints /Out/, bool splitFeature = true ); + Qgis::GeometryOperationResult splitGeometry( const QgsCurve *curve, QVector &newGeometries /Out/, bool preserveCircular, bool topological, QgsPointSequence &topologyTestPoints /Out/, bool splitFeature = true ); %Docstring Splits this geometry according to a given curve. @@ -1391,11 +1409,7 @@ will be generated. .. versionadded:: 3.24 %End - QgsGeometry applyDashPattern( const QVector< double > &pattern, - Qgis::DashPatternLineEndingRule startRule = Qgis::DashPatternLineEndingRule::NoRule, - Qgis::DashPatternLineEndingRule endRule = Qgis::DashPatternLineEndingRule::NoRule, - Qgis::DashPatternSizeAdjustment adjustment = Qgis::DashPatternSizeAdjustment::ScaleBothDashAndGap, - double patternOffset = 0 ) const; + QgsGeometry applyDashPattern( const QVector< double > &pattern, Qgis::DashPatternLineEndingRule startRule = Qgis::DashPatternLineEndingRule::NoRule, Qgis::DashPatternLineEndingRule endRule = Qgis::DashPatternLineEndingRule::NoRule, Qgis::DashPatternSizeAdjustment adjustment = Qgis::DashPatternSizeAdjustment::ScaleBothDashAndGap, double patternOffset = 0 ) const; %Docstring Applies a dash pattern to a geometry, returning a MultiLineString geometry which is the input geometry stroked along each line/ring with @@ -1629,9 +1643,7 @@ Returns an offset line at a given distance and side from an input line. (JoinStyleMiter only) %End - QgsGeometry singleSidedBuffer( double distance, int segments, Qgis::BufferSide side, - Qgis::JoinStyle joinStyle = Qgis::JoinStyle::Round, - double miterLimit = 2.0 ) const; + QgsGeometry singleSidedBuffer( double distance, int segments, Qgis::BufferSide side, Qgis::JoinStyle joinStyle = Qgis::JoinStyle::Round, double miterLimit = 2.0 ) const; %Docstring Returns a single sided buffer for a (multi)line geometry. The buffer is only applied to one side of the line. @@ -2358,6 +2370,8 @@ the specified type. E.g. - curved geometries will be segmented if ``type`` is non-curved. - multi geometries will be converted to a list of single geometries - single geometries will be upgraded to multi geometries +- PolyhedralSurface/TIN will be converted to multi polygon +- multipolygon, polygon and triangle will be converted to TIN - z or m values will be added or dropped as required. Since QGIS 3.24, the parameters ``defaultZ`` and ``defaultM`` control @@ -2375,6 +2389,12 @@ geometries to points. By default duplicated nodes are ignored. to coerce geometries to the desired ``type``. It also correctly maintains curves and z/m values wherever appropriate. +.. note:: + + If an error occurs during conversion (e.g., attempting to convert a polygon with + non-triangular vertices to a Triangle or TIN geometry), an empty vector will be returned and the + error message can be retrieved by calling :py:func:`~QgsGeometry.lastError` on the returned geometry. + .. versionadded:: 3.14 %End @@ -3093,18 +3113,14 @@ should match. int state1; int sipIsErr = 0; - if ( PyList_Check( a0 ) && PyList_Check( a1 ) && - PyList_GET_SIZE( a0 ) && PyList_GET_SIZE( a1 ) ) + if ( PyList_Check( a0 ) && PyList_Check( a1 ) && PyList_GET_SIZE( a0 ) && PyList_GET_SIZE( a1 ) ) { PyObject *o0 = PyList_GetItem( a0, 0 ); PyObject *o1 = PyList_GetItem( a1, 0 ); if ( o0 && o1 ) { // compare polyline - polyline - if ( sipCanConvertToType( o0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( o1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( o0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( o1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsPolylineXY *p0; QgsPolylineXY *p1; @@ -3117,18 +3133,14 @@ should match. sipReleaseType( p0, sipType_QVector_0100QgsPointXY, state0 ); sipReleaseType( p1, sipType_QVector_0100QgsPointXY, state1 ); } - else if ( PyList_Check( o0 ) && PyList_Check( o1 ) && - PyList_GET_SIZE( o0 ) && PyList_GET_SIZE( o1 ) ) + else if ( PyList_Check( o0 ) && PyList_Check( o1 ) && PyList_GET_SIZE( o0 ) && PyList_GET_SIZE( o1 ) ) { PyObject *oo0 = PyList_GetItem( o0, 0 ); PyObject *oo1 = PyList_GetItem( o1, 0 ); if ( oo0 && oo1 ) { // compare polygon - polygon - if ( sipCanConvertToType( oo0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( oo1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( oo0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( oo1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsPolygonXY *p0; QgsPolygonXY *p1; @@ -3141,18 +3153,14 @@ should match. sipReleaseType( p0, sipType_QVector_0600QVector_0100QgsPointXY, state0 ); sipReleaseType( p1, sipType_QVector_0600QVector_0100QgsPointXY, state1 ); } - else if ( PyList_Check( oo0 ) && PyList_Check( oo1 ) && - PyList_GET_SIZE( oo0 ) && PyList_GET_SIZE( oo1 ) ) + else if ( PyList_Check( oo0 ) && PyList_Check( oo1 ) && PyList_GET_SIZE( oo0 ) && PyList_GET_SIZE( oo1 ) ) { PyObject *ooo0 = PyList_GetItem( oo0, 0 ); PyObject *ooo1 = PyList_GetItem( oo1, 0 ); if ( ooo0 && ooo1 ) { // compare multipolygon - multipolygon - if ( sipCanConvertToType( ooo0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( ooo1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( ooo0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( ooo1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsMultiPolygonXY *p0; QgsMultiPolygonXY *p1; @@ -3174,8 +3182,7 @@ should match. } %End - QgsGeometry smooth( unsigned int iterations = 1, double offset = 0.25, - double minimumDistance = -1.0, double maxAngle = 180.0 ) const; + QgsGeometry smooth( unsigned int iterations = 1, double offset = 0.25, double minimumDistance = -1.0, double maxAngle = 180.0 ) const; %Docstring Smooths a geometry by rounding off corners using the Chaikin algorithm. This operation roughly doubles the number of vertices in a geometry. diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 635b723efab7..e5c5166fafaa 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -93,6 +93,9 @@ #include "qgsfixattributedialog.h" #include "qgsprojecttimesettings.h" #include "qgsgeometrycollection.h" +#include "qgspolyhedralsurface.h" +#include "qgstriangle.h" +#include "qgstriangulatedsurface.h" #include "maptools/qgsappmaptools.h" #include "qgsexpressioncontextutils.h" #include "qgsauxiliarystorage.h" @@ -8877,6 +8880,24 @@ QgsGeometry QgisApp::unionGeometries( const QgsVectorLayer *vl, QgsFeatureList & if ( !featureList.at( 0 ).hasGeometry() ) return QgsGeometry(); + const Qgis::WkbType layerFlatType = QgsWkbTypes::flatType( vl->wkbType() ); + + // Special handling for TIN: collect patches instead of geometric union + if ( layerFlatType == Qgis::WkbType::TIN ) + { + QgsTemporaryCursorOverride waitCursor( Qt::WaitCursor ); + + QVector geometries; + geometries.reserve( featureList.size() ); + for ( const QgsFeature &feature : std::as_const( featureList ) ) + { + geometries.append( feature.geometry() ); + } + + return QgsGeometry::collectTinPatches( geometries ); + } + + // Standard handling for other geometry types: use GEOS combine QgsGeometry unionGeom = featureList.at( 0 ).geometry(); QProgressDialog progress( tr( "Merging features…" ), tr( "Abort" ), 0, featureList.size(), this ); @@ -8911,6 +8932,16 @@ QgsGeometry QgisApp::unionGeometries( const QgsVectorLayer *vl, QgsFeatureList & unionGeom.convertToMultiType(); } + // Convert result to PolyhedralSurface if layer type requires it + if ( layerFlatType == Qgis::WkbType::PolyhedralSurface ) + { + QVector converted = unionGeom.coerceToType( vl->wkbType() ); + if ( !converted.isEmpty() ) + { + unionGeom = converted.at( 0 ); + } + } + QApplication::restoreOverrideCursor(); progress.setValue( featureList.size() ); return unionGeom; diff --git a/src/app/qgsmaptooladdpart.cpp b/src/app/qgsmaptooladdpart.cpp index 6637f1164660..2eb087faea48 100644 --- a/src/app/qgsmaptooladdpart.cpp +++ b/src/app/qgsmaptooladdpart.cpp @@ -211,10 +211,13 @@ QgsVectorLayer *QgsMapToolAddPart::getLayerAndCheckSelection() { // Only one selected feature // For single-type layers only allow features without geometry + // Exception: TIN and PolyhedralSurface can hold multiple patches even though they are "single" types QgsFeatureIterator selectedFeatures = layer->getSelectedFeatures(); QgsFeature selectedFeature; selectedFeatures.nextFeature( selectedFeature ); - if ( QgsWkbTypes::isSingleType( layer->wkbType() ) && selectedFeature.geometry().constGet() ) + const Qgis::WkbType layerFlatType = QgsWkbTypes::flatType( layer->wkbType() ); + const bool isSurfaceWithPatches = ( layerFlatType == Qgis::WkbType::TIN || layerFlatType == Qgis::WkbType::PolyhedralSurface ); + if ( QgsWkbTypes::isSingleType( layer->wkbType() ) && !isSurfaceWithPatches && selectedFeature.geometry().constGet() ) { selectionErrorMsg = tr( "This layer does not support multipart geometries." ); } diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index f24f5074983a..483eac205b58 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -43,6 +43,7 @@ email : morb at ozemail dot com dot au #include "qgspolyhedralsurface.h" #include "qgsrectangle.h" #include "qgstriangle.h" +#include "qgstriangulatedsurface.h" #include "qgsvectorlayer.h" #include @@ -410,6 +411,61 @@ QgsGeometry QgsGeometry::collectGeometry( const QVector< QgsGeometry > &geometri return collected; } +QgsGeometry QgsGeometry::collectTinPatches( const QVector &geometries ) +{ + auto resultTin = std::make_unique(); + bool first = true; + + for ( const QgsGeometry &geom : geometries ) + { + if ( geom.isNull() ) + continue; + + const QgsAbstractGeometry *abstractGeom = geom.constGet(); + + if ( const QgsTriangulatedSurface *tin = qgsgeometry_cast( abstractGeom ) ) + { + // Preserve Z/M from first valid geometry + if ( first ) + { + if ( tin->is3D() ) + resultTin->addZValue( 0 ); + if ( tin->isMeasure() ) + resultTin->addMValue( 0 ); + first = false; + } + + // Copy all patches (triangles) from the TIN + for ( int j = 0; j < tin->numPatches(); ++j ) + { + if ( const QgsPolygon *patch = tin->patchN( j ) ) + { + resultTin->addPatch( patch->clone() ); + } + } + } + else if ( const QgsTriangle *triangle = qgsgeometry_cast( abstractGeom ) ) + { + // Preserve Z/M from first valid geometry + if ( first ) + { + if ( triangle->is3D() ) + resultTin->addZValue( 0 ); + if ( triangle->isMeasure() ) + resultTin->addMValue( 0 ); + first = false; + } + + resultTin->addPatch( triangle->clone() ); + } + } + + if ( resultTin->numPatches() == 0 ) + return QgsGeometry(); + + return QgsGeometry( std::move( resultTin ) ); +} + QgsGeometry QgsGeometry::createWedgeBuffer( const QgsPoint ¢er, const double azimuth, const double angularWidth, const double outerRadius, const double innerRadius ) { const double startAngle = azimuth - angularWidth * 0.5; @@ -1039,6 +1095,7 @@ Qgis::GeometryOperationResult QgsGeometry::addPartV2( QgsAbstractGeometry *part, std::unique_ptr< QgsAbstractGeometry > p( part ); if ( !d->geometry ) { + // NOLINTBEGIN(bugprone-branch-clone) switch ( QgsWkbTypes::singleType( QgsWkbTypes::flatType( wkbType ) ) ) { case Qgis::WkbType::Point: @@ -1058,15 +1115,27 @@ Qgis::GeometryOperationResult QgsGeometry::addPartV2( QgsAbstractGeometry *part, case Qgis::WkbType::CircularString: reset( std::make_unique< QgsMultiCurve >() ); break; + case Qgis::WkbType::PolyhedralSurface: + reset( std::make_unique< QgsPolyhedralSurface >() ); + break; + case Qgis::WkbType::TIN: + reset( std::make_unique< QgsTriangulatedSurface >() ); + break; default: reset( nullptr ); return Qgis::GeometryOperationResult::AddPartNotMultiGeometry; + // NOLINTEND(bugprone-branch-clone) } } else { detach(); - convertToMultiType(); + // For TIN and PolyhedralSurface, they already support multiple patches, no conversion needed + const Qgis::WkbType flatType = QgsWkbTypes::flatType( d->geometry->wkbType() ); + if ( flatType != Qgis::WkbType::TIN && flatType != Qgis::WkbType::PolyhedralSurface ) + { + convertToMultiType(); + } } return QgsGeometryEditUtils::addPart( d->geometry.get(), std::move( p ) ); @@ -1719,6 +1788,7 @@ json QgsGeometry::asJsonObject( int precision ) const QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double defaultZ, double defaultM, bool avoidDuplicates ) const { + mLastError.clear(); QVector< QgsGeometry > res; if ( isNull() ) return res; @@ -1814,17 +1884,82 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double newGeom = QgsGeometry( std::move( polySurface ) ); } + //(Multi)Polygon/Triangle to TIN + if ( QgsWkbTypes::flatType( type ) == Qgis::WkbType::TIN && + ( QgsWkbTypes::flatType( QgsWkbTypes::singleType( newGeom.wkbType() ) ) == Qgis::WkbType::Polygon || + QgsWkbTypes::flatType( QgsWkbTypes::singleType( newGeom.wkbType() ) ) == Qgis::WkbType::Triangle ) ) + { + auto tin = std::make_unique< QgsTriangulatedSurface >(); + const QgsGeometry source = newGeom; + for ( auto part = source.const_parts_begin(); part != source.const_parts_end(); ++part ) + { + if ( const QgsTriangle *triangle = qgsgeometry_cast< const QgsTriangle * >( *part ) ) + { + tin->addPatch( triangle->clone() ); + } + else if ( const QgsPolygon *polygon = qgsgeometry_cast< const QgsPolygon * >( *part ) ) + { + // Validate that the polygon can be converted to a triangle (must have exactly 3 vertices + closing point) + if ( polygon->exteriorRing() ) + { + const int numPoints = polygon->exteriorRing()->numPoints(); + if ( numPoints != 4 ) + { + mLastError = QObject::tr( "Cannot convert polygon with %1 vertices to a triangle. A triangle requires exactly 3 vertices." ).arg( numPoints > 0 ? numPoints - 1 : 0 ); + return res; + } + auto triangle = std::make_unique< QgsTriangle >(); + triangle->setExteriorRing( polygon->exteriorRing()->clone() ); + tin->addPatch( triangle.release() ); + } + } + } + newGeom = QgsGeometry( std::move( tin ) ); + } + + // PolyhedralSurface/TIN to (Multi)Polygon + if ( QgsWkbTypes::flatType( QgsWkbTypes::singleType( type ) ) == Qgis::WkbType::Polygon && + ( QgsWkbTypes::flatType( newGeom.wkbType() ) == Qgis::WkbType::PolyhedralSurface || + QgsWkbTypes::flatType( newGeom.wkbType() ) == Qgis::WkbType::TIN ) ) + { + auto multiPolygon = std::make_unique< QgsMultiPolygon >(); + if ( const QgsPolyhedralSurface *polySurface = qgsgeometry_cast< const QgsPolyhedralSurface * >( newGeom.constGet() ) ) + { + for ( int i = 0; i < polySurface->numPatches(); ++i ) + { + const QgsPolygon *patch = polySurface->patchN( i ); + auto polygon = std::make_unique< QgsPolygon >(); + polygon->setExteriorRing( patch->exteriorRing()->clone() ); + for ( int j = 0; j < patch->numInteriorRings(); ++j ) + { + polygon->addInteriorRing( patch->interiorRing( j )->clone() ); + } + multiPolygon->addGeometry( polygon.release() ); + } + } + newGeom = QgsGeometry( std::move( multiPolygon ) ); + } + // Polygon -> Triangle if ( QgsWkbTypes::flatType( type ) == Qgis::WkbType::Triangle && QgsWkbTypes::flatType( newGeom.wkbType() ) == Qgis::WkbType::Polygon ) { - auto triangle = std::make_unique< QgsTriangle >(); - const QgsGeometry source = newGeom; if ( const QgsPolygon *polygon = qgsgeometry_cast< const QgsPolygon * >( newGeom.constGet() ) ) { - triangle->setExteriorRing( polygon->exteriorRing()->clone() ); + // Validate that the polygon can be converted to a triangle (must have exactly 3 vertices + closing point) + if ( polygon->exteriorRing() ) + { + const int numPoints = polygon->exteriorRing()->numPoints(); + if ( numPoints != 4 ) + { + mLastError = QObject::tr( "Cannot convert polygon with %1 vertices to a triangle. A triangle requires exactly 3 vertices." ).arg( numPoints > 0 ? numPoints - 1 : 0 ); + return res; + } + auto triangle = std::make_unique< QgsTriangle >(); + triangle->setExteriorRing( polygon->exteriorRing()->clone() ); + newGeom = QgsGeometry( std::move( triangle ) ); + } } - newGeom = QgsGeometry( std::move( triangle ) ); } @@ -3786,6 +3921,27 @@ static bool vertexIndexInfo( const QgsAbstractGeometry *g, int vertexIndex, int partIndex++; } } + else if ( const QgsPolyhedralSurface *polySurface = qgsgeometry_cast( g ) ) + { + // PolyhedralSurface: patches are the parts + partIndex = 0; + for ( int i = 0; i < polySurface->numPatches(); ++i ) + { + const QgsPolygon *patch = polySurface->patchN( i ); + // count total number of vertices in the patch + int numPoints = 0; + for ( int k = 0; k < patch->ringCount(); ++k ) + numPoints += patch->vertexCount( 0, k ); + + if ( vertexIndex < numPoints ) + { + int nothing; + return vertexIndexInfo( patch, vertexIndex, nothing, ringIndex, vertex ); + } + vertexIndex -= numPoints; + partIndex++; + } + } else if ( const QgsCurvePolygon *curvePolygon = qgsgeometry_cast( g ) ) { const QgsCurve *ring = curvePolygon->exteriorRing(); @@ -3854,6 +4010,10 @@ bool QgsGeometry::vertexIdFromVertexNr( int nr, QgsVertexId &id ) const { g = geomCollection->geometryN( id.part ); } + else if ( const QgsPolyhedralSurface *polySurface = qgsgeometry_cast( g ) ) + { + g = polySurface->patchN( id.part ); + } if ( const QgsCurvePolygon *curvePolygon = qgsgeometry_cast( g ) ) { diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index a773128add36..d3adbbe67840 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -124,7 +124,6 @@ struct QgsGeometryPrivate; class CORE_EXPORT QgsGeometryParameters { public: - /** * Returns the grid size which will be used to snap vertices of a geometry. * @@ -150,7 +149,6 @@ class CORE_EXPORT QgsGeometryParameters void setGridSize( double size ) { mGridSize = size; } private: - double mGridSize = -1; }; @@ -181,7 +179,6 @@ class CORE_EXPORT QgsGeometry Q_PROPERTY( Qgis::GeometryType type READ type ) public: - QgsGeometry() SIP_HOLDGIL; //! Copy constructor will prompt a shallow copy of the geometry @@ -345,6 +342,23 @@ class CORE_EXPORT QgsGeometry //! Creates a new multipart geometry from a list of QgsGeometry objects static QgsGeometry collectGeometry( const QVector &geometries ); + /** + * Collects all patches from a list of TIN or Triangle geometries into a single TIN geometry. + * + * This method iterates through the input \a geometries and extracts all triangle patches, + * combining them into a single QgsTriangulatedSurface. Input geometries can be either + * QgsTriangulatedSurface (TIN) or QgsTriangle objects. Other geometry types are ignored. + * + * The resulting TIN will preserve Z and M values if present in the first valid input geometry. + * + * \param geometries list of input geometries (should be TIN or Triangle types) + * \returns a QgsGeometry containing a QgsTriangulatedSurface with all collected patches, + * or a null geometry if no valid TIN/Triangle geometries were found + * + * \since QGIS 4.0 + */ + static QgsGeometry collectTinPatches( const QVector &geometries ); + /** * Creates a wedge shaped buffer from a \a center point. * @@ -360,8 +374,7 @@ class CORE_EXPORT QgsGeometry * * \since QGIS 3.2 */ - static QgsGeometry createWedgeBuffer( const QgsPoint ¢er, double azimuth, double angularWidth, - double outerRadius, double innerRadius = 0 ); + static QgsGeometry createWedgeBuffer( const QgsPoint ¢er, double azimuth, double angularWidth, double outerRadius, double innerRadius = 0 ); /** * Creates a wedge shaped buffer from a \a center point. @@ -376,8 +389,7 @@ class CORE_EXPORT QgsGeometry * * \since QGIS 3.40 */ - static QgsGeometry createWedgeBufferFromAngles( const QgsPoint ¢er, double startAngle, double endAngle, - double outerRadius, double innerRadius = 0 ); + static QgsGeometry createWedgeBufferFromAngles( const QgsPoint ¢er, double startAngle, double endAngle, double outerRadius, double innerRadius = 0 ); /** * Set the geometry, feeding in the buffer containing OGC Well-Known Binary and the buffer's length. @@ -1131,8 +1143,7 @@ class CORE_EXPORT QgsGeometry if ( PyList_Check( a0 ) && PyList_GET_SIZE( a0 ) ) { PyObject *p0 = PyList_GetItem( a0, 0 ); - if ( sipCanConvertToType( p0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( p0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QVector newGeometries; QVector topologyTestPoints; @@ -1154,8 +1165,7 @@ class CORE_EXPORT QgsGeometry sipReleaseType( splitLine, sipType_QVector_0100QgsPointXY, state ); } - else if ( sipCanConvertToType( p0, sipType_QgsPoint, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPoint, SIP_NOT_NONE ) ) + else if ( sipCanConvertToType( p0, sipType_QgsPoint, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPoint, SIP_NOT_NONE ) ) { QVector newGeometries; QVector topologyTestPoints; @@ -1192,17 +1202,17 @@ class CORE_EXPORT QgsGeometry #endif /** - * Splits this geometry according to a given curve. - * \param curve the curve that splits the geometry - * \param[out] newGeometries list of new geometries that have been created with the ``splitLine``. If the geometry is 3D, a linear interpolation of the z value is performed on the geometry at split points, see example. - * \param preserveCircular whether if circular strings are preserved after splitting - * \param topological TRUE if topological editing is enabled - * \param[out] topologyTestPoints points that need to be tested for topological completeness in the dataset - * \param splitFeature Set to TRUE if you want to split a feature, otherwise set to FALSE to split parts - * \returns OperationResult a result code: success or reason of failure - * \since QGIS 3.16 - */ - Qgis::GeometryOperationResult splitGeometry( const QgsCurve *curve, QVector &newGeometries SIP_OUT, bool preserveCircular, bool topological, QgsPointSequence &topologyTestPoints SIP_OUT, bool splitFeature = true ); + * Splits this geometry according to a given curve. + * \param curve the curve that splits the geometry + * \param[out] newGeometries list of new geometries that have been created with the ``splitLine``. If the geometry is 3D, a linear interpolation of the z value is performed on the geometry at split points, see example. + * \param preserveCircular whether if circular strings are preserved after splitting + * \param topological TRUE if topological editing is enabled + * \param[out] topologyTestPoints points that need to be tested for topological completeness in the dataset + * \param splitFeature Set to TRUE if you want to split a feature, otherwise set to FALSE to split parts + * \returns OperationResult a result code: success or reason of failure + * \since QGIS 3.16 + */ + Qgis::GeometryOperationResult splitGeometry( const QgsCurve *curve, QVector &newGeometries SIP_OUT, bool preserveCircular, bool topological, QgsPointSequence &topologyTestPoints SIP_OUT, bool splitFeature = true ); /** * Replaces a part of this geometry with another line @@ -1402,11 +1412,7 @@ class CORE_EXPORT QgsGeometry * * \since QGIS 3.24 */ - QgsGeometry applyDashPattern( const QVector< double > &pattern, - Qgis::DashPatternLineEndingRule startRule = Qgis::DashPatternLineEndingRule::NoRule, - Qgis::DashPatternLineEndingRule endRule = Qgis::DashPatternLineEndingRule::NoRule, - Qgis::DashPatternSizeAdjustment adjustment = Qgis::DashPatternSizeAdjustment::ScaleBothDashAndGap, - double patternOffset = 0 ) const; + QgsGeometry applyDashPattern( const QVector< double > &pattern, Qgis::DashPatternLineEndingRule startRule = Qgis::DashPatternLineEndingRule::NoRule, Qgis::DashPatternLineEndingRule endRule = Qgis::DashPatternLineEndingRule::NoRule, Qgis::DashPatternSizeAdjustment adjustment = Qgis::DashPatternSizeAdjustment::ScaleBothDashAndGap, double patternOffset = 0 ) const; /** * Returns a new geometry with all points or vertices snapped to the closest point of the grid. @@ -1612,9 +1618,7 @@ class CORE_EXPORT QgsGeometry * \see buffer() * \see taperedBuffer() */ - QgsGeometry singleSidedBuffer( double distance, int segments, Qgis::BufferSide side, - Qgis::JoinStyle joinStyle = Qgis::JoinStyle::Round, - double miterLimit = 2.0 ) const; + QgsGeometry singleSidedBuffer( double distance, int segments, Qgis::BufferSide side, Qgis::JoinStyle joinStyle = Qgis::JoinStyle::Round, double miterLimit = 2.0 ) const; /** * Calculates a variable width buffer ("tapered buffer") for a (multi)curve geometry. @@ -2188,7 +2192,7 @@ class CORE_EXPORT QgsGeometry #endif -///@endcond + ///@endcond /** * Returns the length of the QByteArray returned by asWkb() @@ -2232,8 +2236,8 @@ class CORE_EXPORT QgsGeometry #endif /** - * Exports the geometry to a GeoJSON string. - */ + * Exports the geometry to a GeoJSON string. + */ QString asJson( int precision = 17 ) const; /** @@ -2256,6 +2260,8 @@ class CORE_EXPORT QgsGeometry * - curved geometries will be segmented if \a type is non-curved. * - multi geometries will be converted to a list of single geometries * - single geometries will be upgraded to multi geometries + * - PolyhedralSurface/TIN will be converted to multi polygon + * - multipolygon, polygon and triangle will be converted to TIN * - z or m values will be added or dropped as required. * * Since QGIS 3.24, the parameters \a defaultZ and \a defaultM control the dimension value added when promoting geometries @@ -2271,6 +2277,10 @@ class CORE_EXPORT QgsGeometry * to coerce geometries to the desired \a type. It also correctly maintains curves and z/m values * wherever appropriate. * + * \note If an error occurs during conversion (e.g., attempting to convert a polygon with + * non-triangular vertices to a Triangle or TIN geometry), an empty vector will be returned and the + * error message can be retrieved by calling lastError() on the returned geometry. + * * \since QGIS 3.14 */ QVector< QgsGeometry > coerceToType( Qgis::WkbType type, double defaultZ = 0, double defaultM = 0, bool avoidDuplicates = true ) const; @@ -2350,16 +2360,16 @@ class CORE_EXPORT QgsGeometry #else /** - * Returns the contents of the geometry as a polyline. - * - * Any z or m values present in the geometry will be discarded. If the geometry is a curved line type - * (such as a CircularString), it will be automatically segmentized. - * - * This method works only with single-line (or single-curve). - * - * \throws TypeError if the geometry is not a single-line type - * \throws ValueError if the geometry is null - */ + * Returns the contents of the geometry as a polyline. + * + * Any z or m values present in the geometry will be discarded. If the geometry is a curved line type + * (such as a CircularString), it will be automatically segmentized. + * + * This method works only with single-line (or single-curve). + * + * \throws TypeError if the geometry is not a single-line type + * \throws ValueError if the geometry is null + */ SIP_PYOBJECT asPolyline() const SIP_TYPEHINT( QgsPolylineXY ); % MethodCode const Qgis::WkbType type = sipCpp->wkbType(); @@ -2395,16 +2405,16 @@ class CORE_EXPORT QgsGeometry #else /** - * Returns the contents of the geometry as a polygon. - * - * Any z or m values present in the geometry will be discarded. If the geometry is a curved polygon type - * (such as a CurvePolygon), it will be automatically segmentized. - * - * This method works only with single-polygon (or single-curve polygon) geometry types. - * - * \throws TypeError if the geometry is not a single-polygon type - * \throws ValueError if the geometry is null - */ + * Returns the contents of the geometry as a polygon. + * + * Any z or m values present in the geometry will be discarded. If the geometry is a curved polygon type + * (such as a CurvePolygon), it will be automatically segmentized. + * + * This method works only with single-polygon (or single-curve polygon) geometry types. + * + * \throws TypeError if the geometry is not a single-polygon type + * \throws ValueError if the geometry is null + */ SIP_PYOBJECT asPolygon() const SIP_TYPEHINT( QgsPolygonXY ); % MethodCode const Qgis::WkbType type = sipCpp->wkbType(); @@ -2439,15 +2449,15 @@ class CORE_EXPORT QgsGeometry #else /** - * Returns the contents of the geometry as a multi-point. - * - * Any z or m values present in the geometry will be discarded. - * - * This method works only with multi-point geometry types. - * - * \throws TypeError if the geometry is not a multi-point type - * \throws ValueError if the geometry is null - */ + * Returns the contents of the geometry as a multi-point. + * + * Any z or m values present in the geometry will be discarded. + * + * This method works only with multi-point geometry types. + * + * \throws TypeError if the geometry is not a multi-point type + * \throws ValueError if the geometry is null + */ SIP_PYOBJECT asMultiPoint() const SIP_TYPEHINT( QgsMultiPointXY ); % MethodCode const Qgis::WkbType type = sipCpp->wkbType(); @@ -2483,16 +2493,16 @@ class CORE_EXPORT QgsGeometry #else /** - * Returns the contents of the geometry as a multi-linestring. - * - * Any z or m values present in the geometry will be discarded. If the geometry is a curved line type - * (such as a MultiCurve), it will be automatically segmentized. - * - * This method works only with multi-linestring (or multi-curve) geometry types. - * - * \throws TypeError if the geometry is not a multi-linestring type - * \throws ValueError if the geometry is null - */ + * Returns the contents of the geometry as a multi-linestring. + * + * Any z or m values present in the geometry will be discarded. If the geometry is a curved line type + * (such as a MultiCurve), it will be automatically segmentized. + * + * This method works only with multi-linestring (or multi-curve) geometry types. + * + * \throws TypeError if the geometry is not a multi-linestring type + * \throws ValueError if the geometry is null + */ SIP_PYOBJECT asMultiPolyline() const SIP_TYPEHINT( QgsMultiPolylineXY ); % MethodCode const Qgis::WkbType type = sipCpp->wkbType(); @@ -2528,16 +2538,16 @@ class CORE_EXPORT QgsGeometry #else /** - * Returns the contents of the geometry as a multi-polygon. - * - * Any z or m values present in the geometry will be discarded. If the geometry is a curved polygon type - * (such as a MultiSurface), it will be automatically segmentized. - * - * This method works only with multi-polygon (or multi-curve polygon) geometry types. - * - * \throws TypeError if the geometry is not a multi-polygon type - * \throws ValueError if the geometry is null - */ + * Returns the contents of the geometry as a multi-polygon. + * + * Any z or m values present in the geometry will be discarded. If the geometry is a curved polygon type + * (such as a MultiSurface), it will be automatically segmentized. + * + * This method works only with multi-polygon (or multi-curve polygon) geometry types. + * + * \throws TypeError if the geometry is not a multi-polygon type + * \throws ValueError if the geometry is null + */ SIP_PYOBJECT asMultiPolygon() const SIP_TYPEHINT( QgsMultiPolygonXY ); % MethodCode const Qgis::WkbType type = sipCpp->wkbType(); @@ -2656,8 +2666,7 @@ class CORE_EXPORT QgsGeometry * 4 if the geometry is not intersected by one of the geometries present in the provided layers. * \deprecated QGIS 3.34 */ - Q_DECL_DEPRECATED int avoidIntersections( const QList &avoidIntersectionsLayers, - const QHash > &ignoreFeatures SIP_PYARGREMOVE = ( QHash >() ) ) SIP_DEPRECATED; + Q_DECL_DEPRECATED int avoidIntersections( const QList &avoidIntersectionsLayers, const QHash > &ignoreFeatures SIP_PYARGREMOVE = ( QHash >() ) ) SIP_DEPRECATED; /** * Modifies geometry to avoid intersections with the layers specified in project properties @@ -2670,8 +2679,7 @@ class CORE_EXPORT QgsGeometry * NothingHappened if the geometry is not intersected by one of the geometries present in the provided layers. * \since QGIS 3.34 */ - Qgis::GeometryOperationResult avoidIntersectionsV2( const QList &avoidIntersectionsLayers, - const QHash > &ignoreFeatures SIP_PYARGREMOVE = ( QHash >() ) ); + Qgis::GeometryOperationResult avoidIntersectionsV2( const QList &avoidIntersectionsLayers, const QHash > &ignoreFeatures SIP_PYARGREMOVE = ( QHash >() ) ); /** * Attempts to make an invalid geometry valid without losing vertices. @@ -2998,8 +3006,7 @@ class CORE_EXPORT QgsGeometry * \returns TRUE if polylines have the same number of points and all * points are equal within the specified tolerance */ - static bool compare( const QgsPolylineXY &p1, const QgsPolylineXY &p2, - double epsilon = 4 * std::numeric_limits::epsilon() ); + static bool compare( const QgsPolylineXY &p1, const QgsPolylineXY &p2, double epsilon = 4 * std::numeric_limits::epsilon() ); /** * Compares two polygons for equality within a specified tolerance. @@ -3009,8 +3016,7 @@ class CORE_EXPORT QgsGeometry * \returns TRUE if polygons have the same number of rings, and each ring has the same * number of points and all points are equal within the specified tolerance */ - static bool compare( const QgsPolygonXY &p1, const QgsPolygonXY &p2, - double epsilon = 4 * std::numeric_limits::epsilon() ); + static bool compare( const QgsPolygonXY &p1, const QgsPolygonXY &p2, double epsilon = 4 * std::numeric_limits::epsilon() ); /** * Compares two multipolygons for equality within a specified tolerance. @@ -3021,8 +3027,7 @@ class CORE_EXPORT QgsGeometry * of rings, and each ring has the same number of points and all points are equal within the specified * tolerance */ - static bool compare( const QgsMultiPolygonXY &p1, const QgsMultiPolygonXY &p2, - double epsilon = 4 * std::numeric_limits::epsilon() ); + static bool compare( const QgsMultiPolygonXY &p1, const QgsMultiPolygonXY &p2, double epsilon = 4 * std::numeric_limits::epsilon() ); #else /** @@ -3051,18 +3056,14 @@ class CORE_EXPORT QgsGeometry int state1; int sipIsErr = 0; - if ( PyList_Check( a0 ) && PyList_Check( a1 ) && - PyList_GET_SIZE( a0 ) && PyList_GET_SIZE( a1 ) ) + if ( PyList_Check( a0 ) && PyList_Check( a1 ) && PyList_GET_SIZE( a0 ) && PyList_GET_SIZE( a1 ) ) { PyObject *o0 = PyList_GetItem( a0, 0 ); PyObject *o1 = PyList_GetItem( a1, 0 ); if ( o0 && o1 ) { // compare polyline - polyline - if ( sipCanConvertToType( o0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( o1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( o0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( o1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsPolylineXY *p0; QgsPolylineXY *p1; @@ -3075,18 +3076,14 @@ class CORE_EXPORT QgsGeometry sipReleaseType( p0, sipType_QVector_0100QgsPointXY, state0 ); sipReleaseType( p1, sipType_QVector_0100QgsPointXY, state1 ); } - else if ( PyList_Check( o0 ) && PyList_Check( o1 ) && - PyList_GET_SIZE( o0 ) && PyList_GET_SIZE( o1 ) ) + else if ( PyList_Check( o0 ) && PyList_Check( o1 ) && PyList_GET_SIZE( o0 ) && PyList_GET_SIZE( o1 ) ) { PyObject *oo0 = PyList_GetItem( o0, 0 ); PyObject *oo1 = PyList_GetItem( o1, 0 ); if ( oo0 && oo1 ) { // compare polygon - polygon - if ( sipCanConvertToType( oo0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( oo1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( oo0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( oo1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsPolygonXY *p0; QgsPolygonXY *p1; @@ -3099,18 +3096,14 @@ class CORE_EXPORT QgsGeometry sipReleaseType( p0, sipType_QVector_0600QVector_0100QgsPointXY, state0 ); sipReleaseType( p1, sipType_QVector_0600QVector_0100QgsPointXY, state1 ); } - else if ( PyList_Check( oo0 ) && PyList_Check( oo1 ) && - PyList_GET_SIZE( oo0 ) && PyList_GET_SIZE( oo1 ) ) + else if ( PyList_Check( oo0 ) && PyList_Check( oo1 ) && PyList_GET_SIZE( oo0 ) && PyList_GET_SIZE( oo1 ) ) { PyObject *ooo0 = PyList_GetItem( oo0, 0 ); PyObject *ooo1 = PyList_GetItem( oo1, 0 ); if ( ooo0 && ooo1 ) { // compare multipolygon - multipolygon - if ( sipCanConvertToType( ooo0, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( ooo1, sipType_QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a0, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && - sipCanConvertToType( a1, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) + if ( sipCanConvertToType( ooo0, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( ooo1, sipType_QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a0, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) && sipCanConvertToType( a1, sipType_QVector_0600QVector_0600QVector_0100QgsPointXY, SIP_NOT_NONE ) ) { QgsMultiPolygonXY *p0; QgsMultiPolygonXY *p1; @@ -3148,8 +3141,7 @@ class CORE_EXPORT QgsGeometry * \param minimumDistance minimum segment length to apply smoothing to * \param maxAngle maximum angle at node (0-180) at which smoothing will be applied */ - QgsGeometry smooth( unsigned int iterations = 1, double offset = 0.25, - double minimumDistance = -1.0, double maxAngle = 180.0 ) const; + QgsGeometry smooth( unsigned int iterations = 1, double offset = 0.25, double minimumDistance = -1.0, double maxAngle = 180.0 ) const; /** * Creates and returns a new geometry engine representing the specified \a geometry using \a precision on a grid. The \a precision argument was added in 3.36. @@ -3300,7 +3292,6 @@ class CORE_EXPORT QgsGeometry private: - QgsGeometryPrivate *d; //implicitly shared data pointer //! Last error encountered @@ -3338,8 +3329,7 @@ class CORE_EXPORT QgsGeometry * \param minimumDistance minimum segment length to apply smoothing to * \param maxAngle maximum angle at node (0-180) at which smoothing will be applied */ - std::unique_ptr< QgsLineString > smoothLine( const QgsLineString &line, unsigned int iterations = 1, double offset = 0.25, - double minimumDistance = -1, double maxAngle = 180.0 ) const; + std::unique_ptr< QgsLineString > smoothLine( const QgsLineString &line, unsigned int iterations = 1, double offset = 0.25, double minimumDistance = -1, double maxAngle = 180.0 ) const; /** * Smooths a polygon using the Chaikin algorithm @@ -3352,8 +3342,7 @@ class CORE_EXPORT QgsGeometry * \param minimumDistance minimum segment length to apply smoothing to * \param maxAngle maximum angle at node (0-180) at which smoothing will be applied */ - std::unique_ptr< QgsPolygon > smoothPolygon( const QgsPolygon &polygon, unsigned int iterations = 1, double offset = 0.25, - double minimumDistance = -1, double maxAngle = 180.0 ) const; + std::unique_ptr< QgsPolygon > smoothPolygon( const QgsPolygon &polygon, unsigned int iterations = 1, double offset = 0.25, double minimumDistance = -1, double maxAngle = 180.0 ) const; QgsGeometry doChamferFillet( ChamferFilletOperationType op, int vertexIndex, double distance1, double distance2, int segments ) const; diff --git a/src/core/geometry/qgsgeometryeditutils.cpp b/src/core/geometry/qgsgeometryeditutils.cpp index 6e56b2c9d5d2..6f429fc89aff 100644 --- a/src/core/geometry/qgsgeometryeditutils.cpp +++ b/src/core/geometry/qgsgeometryeditutils.cpp @@ -24,6 +24,9 @@ email : marco.hugentobler at sourcepole dot com #include "qgsgeometrycollection.h" #include "qgsgeometryengine.h" #include "qgspolygon.h" +#include "qgspolyhedralsurface.h" +#include "qgstriangle.h" +#include "qgstriangulatedsurface.h" #include "qgsvectorlayer.h" Qgis::GeometryOperationResult QgsGeometryEditUtils::addRing( QgsAbstractGeometry *geom, std::unique_ptr ring ) @@ -36,10 +39,21 @@ Qgis::GeometryOperationResult QgsGeometryEditUtils::addRing( QgsAbstractGeometry QVector< QgsCurvePolygon * > polygonList; QgsCurvePolygon *curvePoly = qgsgeometry_cast< QgsCurvePolygon * >( geom ); QgsGeometryCollection *multiGeom = qgsgeometry_cast< QgsGeometryCollection * >( geom ); + QgsPolyhedralSurface *polySurface = qgsgeometry_cast< QgsPolyhedralSurface * >( geom ); + if ( curvePoly ) { polygonList.append( curvePoly ); } + else if ( polySurface ) + { + // PolyhedralSurface: collect all patches (which are QgsPolygon* that inherit from QgsCurvePolygon) + polygonList.reserve( polySurface->numPatches() ); + for ( int i = 0; i < polySurface->numPatches(); ++i ) + { + polygonList.append( polySurface->patchN( i ) ); + } + } else if ( multiGeom ) { polygonList.reserve( multiGeom->numGeometries() ); @@ -50,7 +64,7 @@ Qgis::GeometryOperationResult QgsGeometryEditUtils::addRing( QgsAbstractGeometry } else { - return Qgis::GeometryOperationResult::InvalidInputGeometryType; //not polygon / multipolygon; + return Qgis::GeometryOperationResult::InvalidInputGeometryType; //not polygon / multipolygon / polyhedral surface; } //ring must be closed @@ -107,6 +121,91 @@ Qgis::GeometryOperationResult QgsGeometryEditUtils::addPart( QgsAbstractGeometry return Qgis::GeometryOperationResult::InvalidInputGeometryType; } + // Handle TIN - add triangle as patch + if ( QgsWkbTypes::flatType( geom->wkbType() ) == Qgis::WkbType::TIN ) + { + QgsTriangulatedSurface *tin = qgsgeometry_cast( geom ); + if ( !tin ) + { + return Qgis::GeometryOperationResult::InvalidBaseGeometry; + } + + // Part can be a Triangle or a Polygon (that must be a triangle) + if ( QgsTriangle *triangle = qgsgeometry_cast( part.get() ) ) + { + tin->addPatch( triangle->clone() ); + return Qgis::GeometryOperationResult::Success; + } + else if ( QgsPolygon *polygon = qgsgeometry_cast( part.get() ) ) + { + // Validate that the polygon can be converted to a triangle (3 vertices + closing point) + if ( polygon->exteriorRing() && polygon->exteriorRing()->numPoints() == 4 ) + { + auto triangle = std::make_unique(); + triangle->setExteriorRing( polygon->exteriorRing()->clone() ); + if ( !triangle->isEmpty() ) + { + tin->addPatch( triangle.release() ); + return Qgis::GeometryOperationResult::Success; + } + } + return Qgis::GeometryOperationResult::InvalidInputGeometryType; + } + else if ( QgsCurve *curve = qgsgeometry_cast( part.get() ) ) + { + // A closed curve with exactly 4 points (3 vertices + closing) + if ( curve->isClosed() && curve->numPoints() == 4 ) + { + auto triangle = std::make_unique(); + triangle->setExteriorRing( curve->clone() ); + if ( !triangle->isEmpty() ) + { + tin->addPatch( triangle.release() ); + return Qgis::GeometryOperationResult::Success; + } + } + return Qgis::GeometryOperationResult::InvalidInputGeometryType; + } + return Qgis::GeometryOperationResult::InvalidInputGeometryType; + } + + // Handle PolyhedralSurface - add polygon as patch + if ( QgsWkbTypes::flatType( geom->wkbType() ) == Qgis::WkbType::PolyhedralSurface ) + { + QgsPolyhedralSurface *polySurface = qgsgeometry_cast( geom ); + if ( !polySurface ) + { + return Qgis::GeometryOperationResult::InvalidBaseGeometry; + } + + // Part can be a Polygon or a CurvePolygon + if ( QgsPolygon *polygon = qgsgeometry_cast( part.get() ) ) + { + polySurface->addPatch( polygon->clone() ); + return Qgis::GeometryOperationResult::Success; + } + else if ( const QgsCurvePolygon *curvePolygon = qgsgeometry_cast( part.get() ) ) + { + // Convert curve polygon to polygon + std::unique_ptr polygon( curvePolygon->toPolygon() ); + polySurface->addPatch( polygon.release() ); + return Qgis::GeometryOperationResult::Success; + } + else if ( QgsCurve *curve = qgsgeometry_cast( part.get() ) ) + { + // A closed curve becomes a polygon patch + if ( curve->isClosed() && curve->numPoints() >= 4 ) + { + auto polygon = std::make_unique(); + polygon->setExteriorRing( curve->clone() ); + polySurface->addPatch( polygon.release() ); + return Qgis::GeometryOperationResult::Success; + } + return Qgis::GeometryOperationResult::InvalidInputGeometryType; + } + return Qgis::GeometryOperationResult::InvalidInputGeometryType; + } + //multitype? QgsGeometryCollection *geomCollection = qgsgeometry_cast( geom ); if ( !geomCollection ) diff --git a/src/gui/maptools/qgsmaptooldigitizefeature.cpp b/src/gui/maptools/qgsmaptooldigitizefeature.cpp index 70e5803c6922..c89bd540c12c 100644 --- a/src/gui/maptools/qgsmaptooldigitizefeature.cpp +++ b/src/gui/maptools/qgsmaptooldigitizefeature.cpp @@ -80,13 +80,21 @@ void QgsMapToolDigitizeFeature::layerGeometryCaptured( const QgsGeometry &geomet { double defaultZ = QgsSettingsRegistryCore::settingsDigitizingDefaultZValue->value(); double defaultM = QgsSettingsRegistryCore::settingsDigitizingDefaultMValue->value(); - QVector layerGeometries = geometry.coerceToType( layerWKBType, defaultZ, defaultM ); + QVector layerGeometries = geometry.coerceToType( layerWKBType, defaultZ, defaultM, true ); if ( layerGeometries.count() > 0 ) layerGeometry = layerGeometries.at( 0 ); if ( layerGeometry.wkbType() != layerWKBType && layerGeometry.wkbType() != QgsWkbTypes::linearType( layerWKBType ) ) { - emit messageEmitted( tr( "The digitized geometry type (%1) does not correspond to the layer geometry type (%2)." ).arg( QgsWkbTypes::displayString( layerGeometry.wkbType() ), QgsWkbTypes::displayString( layerWKBType ) ), Qgis::MessageLevel::Warning ); + const QString coerceError = geometry.lastError(); + if ( !coerceError.isEmpty() ) + { + emit messageEmitted( coerceError, Qgis::MessageLevel::Warning ); + } + else + { + emit messageEmitted( tr( "The digitized geometry type (%1) does not correspond to the layer geometry type (%2)." ).arg( QgsWkbTypes::displayString( layerGeometry.wkbType() ), QgsWkbTypes::displayString( layerWKBType ) ), Qgis::MessageLevel::Warning ); + } return; } } diff --git a/tests/src/core/geometry/testqgsgeometry.cpp b/tests/src/core/geometry/testqgsgeometry.cpp index 1cd2ead43555..0a9683cd8521 100644 --- a/tests/src/core/geometry/testqgsgeometry.cpp +++ b/tests/src/core/geometry/testqgsgeometry.cpp @@ -38,6 +38,8 @@ #include "qgslinestring.h" #include "qgspolygon.h" #include "qgstriangle.h" +#include "qgstriangulatedsurface.h" +#include "qgspolyhedralsurface.h" #include "qgsgeometryengine.h" #include "qgscircle.h" #include "qgsmultipoint.h" @@ -178,6 +180,8 @@ class TestQgsGeometry : public QgsTest void chamferFillet(); + void collectTinPatches(); + private: //! Must be called before each render test void initPainterTest(); @@ -3322,5 +3326,97 @@ void TestQgsGeometry::chamferFillet() QCOMPARE( g2.asWkt( 2 ), "MultiPolygon (((5 15, 10 15, 10 20, 5 20, 5 15)),((105 15, 108 15, 108.77 15.15, 109.41 15.59, 109.85 16.23, 110 17, 110 20, 105 20, 105 15)))" ); } +void TestQgsGeometry::collectTinPatches() +{ + QgsTriangle triangle1( QgsPoint( 0, 0, 0 ), QgsPoint( 1, 0, 0 ), QgsPoint( 0.5, 1, 0 ) ); + QgsTriangle triangle2( QgsPoint( 1, 0, 0 ), QgsPoint( 2, 0, 0 ), QgsPoint( 1.5, 1, 0 ) ); + + QgsGeometry geom1( triangle1.clone() ); + QgsGeometry geom2( triangle2.clone() ); + + QVector geometries; + geometries << geom1 << geom2; + + QgsGeometry result = QgsGeometry::collectTinPatches( geometries ); + + QVERIFY( !result.isNull() ); + + QVERIFY( result.wkbType() == Qgis::WkbType::TINZ ); + + const QgsTriangulatedSurface *tin = qgsgeometry_cast( result.constGet() ); + QVERIFY( tin != nullptr ); + QCOMPARE( tin->numPatches(), 2 ); + + // Test with mixed TIN and Triangle inputs + QgsTriangulatedSurface tinSurface; + QgsTriangle triangle3( QgsPoint( 2, 0, 0 ), QgsPoint( 3, 0, 0 ), QgsPoint( 2.5, 1, 0 ) ); + tinSurface.addPatch( triangle3.clone() ); + + QgsGeometry geom3( tinSurface.clone() ); + QgsGeometry geom4( triangle1.clone() ); + + QVector mixedGeometries; + mixedGeometries << geom3 << geom4; + + QgsGeometry mixedResult = QgsGeometry::collectTinPatches( mixedGeometries ); + QVERIFY( !mixedResult.isNull() ); + QVERIFY( mixedResult.wkbType() == Qgis::WkbType::TINZ ); + + const QgsTriangulatedSurface *mixedTin = qgsgeometry_cast( mixedResult.constGet() ); + QVERIFY( mixedTin != nullptr ); + QCOMPARE( mixedTin->numPatches(), 2 ); // 1 from the TIN + 1 from the triangle + + // Test with null geometries + QgsGeometry nullGeom; + QVector geometriesWithNull; + geometriesWithNull << geom1 << nullGeom << geom2; + + QgsGeometry resultWithNull = QgsGeometry::collectTinPatches( geometriesWithNull ); + QVERIFY( !resultWithNull.isNull() ); + QVERIFY( resultWithNull.wkbType() == Qgis::WkbType::TINZ ); + + const QgsTriangulatedSurface *tinWithNull = qgsgeometry_cast( resultWithNull.constGet() ); + QVERIFY( tinWithNull != nullptr ); + QCOMPARE( tinWithNull->numPatches(), 2 ); // Should still have 2 patches + + // Test with empty input + QVector emptyGeometries; + QgsGeometry emptyResult = QgsGeometry::collectTinPatches( emptyGeometries ); + QVERIFY( emptyResult.isNull() ); + + // Test with only null geometries + QVector onlyNulls; + onlyNulls << nullGeom << nullGeom; + QgsGeometry onlyNullResult = QgsGeometry::collectTinPatches( onlyNulls ); + QVERIFY( onlyNullResult.isNull() ); + + // Test with non-TIN/non-triangle geometries + QgsGeometry pointGeom = QgsGeometry::fromPointXY( QgsPointXY( 0, 0 ) ); + QgsGeometry lineGeom = QgsGeometry::fromPolylineXY( QgsPolylineXY() << QgsPointXY( 0, 0 ) << QgsPointXY( 1, 1 ) ); + + QVector nonTinGeometries; + nonTinGeometries << pointGeom << lineGeom; + + QgsGeometry nonTinResult = QgsGeometry::collectTinPatches( nonTinGeometries ); + QVERIFY( nonTinResult.isNull() ); + + // Test preserving Z and M values from first valid geometry + QgsTriangle triangleZM( QgsPoint( 0, 0, 10, 20 ), QgsPoint( 1, 0, 11, 21 ), QgsPoint( 0.5, 1, 12, 22 ) ); + QgsGeometry geomZM( triangleZM.clone() ); + + QVector zmGeometries; + zmGeometries << geomZM << geom1; // geom1 has no Z/M + + QgsGeometry zmResult = QgsGeometry::collectTinPatches( zmGeometries ); + QVERIFY( !zmResult.isNull() ); + QCOMPARE( zmResult.wkbType(), Qgis::WkbType::TINZM ); + + const QgsTriangulatedSurface *zmTin = qgsgeometry_cast( zmResult.constGet() ); + QVERIFY( zmTin != nullptr ); + QCOMPARE( zmTin->numPatches(), 2 ); + QVERIFY( zmTin->is3D() ); + QVERIFY( zmTin->isMeasure() ); +} + QGSTEST_MAIN( TestQgsGeometry ) #include "testqgsgeometry.moc" diff --git a/tests/src/core/geometry/testqgspolyhedralsurface.cpp b/tests/src/core/geometry/testqgspolyhedralsurface.cpp index 612802745551..dbbf22ba283a 100644 --- a/tests/src/core/geometry/testqgspolyhedralsurface.cpp +++ b/tests/src/core/geometry/testqgspolyhedralsurface.cpp @@ -12,6 +12,7 @@ * (at your option) any later version. * * * ***************************************************************************/ +#include "qgsgeometry.h" #include "qgslinestring.h" #include "qgsmultilinestring.h" #include "qgsmultipolygon.h" @@ -63,6 +64,8 @@ class TestQgsPolyhedralSurface : public QObject void testExport(); void testCast(); void testIsValid(); + void testGeometryEditUtilsAddPart(); + void testGeometryEditUtilsAddRing(); }; void TestQgsPolyhedralSurface::testConstructor() @@ -1602,6 +1605,76 @@ void TestQgsPolyhedralSurface::testIsValid() QVERIFY( !isValid ); } +void TestQgsPolyhedralSurface::testGeometryEditUtilsAddPart() +{ + // Test Phase 2: QgsGeometry::addPartV2 for PolyhedralSurface + + // Create an empty PolyhedralSurface geometry + QgsGeometry polySurfaceGeom( std::make_unique() ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->numPatches(), 0 ); + + // Add a polygon + QgsPolygon polygon1; + QgsLineString *ring1 = new QgsLineString(); + ring1->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 1, 0 ) << QgsPoint( 1, 1 ) << QgsPoint( 0, 1 ) << QgsPoint( 0, 0 ) ); + polygon1.setExteriorRing( ring1 ); + + Qgis::GeometryOperationResult result = polySurfaceGeom.addPartV2( polygon1.clone(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( result, Qgis::GeometryOperationResult::Success ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->numPatches(), 1 ); + + // Add another polygon + QgsPolygon polygon2; + QgsLineString *ring2 = new QgsLineString(); + ring2->setPoints( QgsPointSequence() << QgsPoint( 2, 0 ) << QgsPoint( 3, 0 ) << QgsPoint( 3, 1 ) << QgsPoint( 2, 1 ) << QgsPoint( 2, 0 ) ); + polygon2.setExteriorRing( ring2 ); + + result = polySurfaceGeom.addPartV2( polygon2.clone(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( result, Qgis::GeometryOperationResult::Success ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->numPatches(), 2 ); + + // Add a closed curve (should be converted to polygon) + QgsLineString curve; + curve.setPoints( QgsPointSequence() << QgsPoint( 4, 0 ) << QgsPoint( 5, 0 ) << QgsPoint( 5, 1 ) << QgsPoint( 4, 1 ) << QgsPoint( 4, 0 ) ); + + result = polySurfaceGeom.addPartV2( curve.clone(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( result, Qgis::GeometryOperationResult::Success ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->numPatches(), 3 ); +} + +void TestQgsPolyhedralSurface::testGeometryEditUtilsAddRing() +{ + // Test Phase 3: QgsGeometry::addRing for PolyhedralSurface + + // Create a PolyhedralSurface with one patch + QgsPolyhedralSurface polySurface; + QgsPolygon *patch = new QgsPolygon(); + QgsLineString *exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 10, 0 ) << QgsPoint( 10, 10 ) << QgsPoint( 0, 10 ) << QgsPoint( 0, 0 ) ); + patch->setExteriorRing( exteriorRing ); + polySurface.addPatch( patch ); + + QgsGeometry polySurfaceGeom( polySurface.clone() ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->numPatches(), 1 ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->patchN( 0 )->numInteriorRings(), 0 ); + + // Add a ring inside the patch + QgsLineString *innerRing = new QgsLineString(); + innerRing->setPoints( QgsPointSequence() << QgsPoint( 2, 2 ) << QgsPoint( 8, 2 ) << QgsPoint( 8, 8 ) << QgsPoint( 2, 8 ) << QgsPoint( 2, 2 ) ); + + Qgis::GeometryOperationResult result = polySurfaceGeom.addRing( innerRing ); + QCOMPARE( result, Qgis::GeometryOperationResult::Success ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->patchN( 0 )->numInteriorRings(), 1 ); + + // Try to add a ring outside all patches - should fail + QgsLineString *outerRing = new QgsLineString(); + outerRing->setPoints( QgsPointSequence() << QgsPoint( 20, 20 ) << QgsPoint( 30, 20 ) << QgsPoint( 30, 30 ) << QgsPoint( 20, 30 ) << QgsPoint( 20, 20 ) ); + + result = polySurfaceGeom.addRing( outerRing ); + QCOMPARE( result, Qgis::GeometryOperationResult::AddRingNotInExistingFeature ); + QCOMPARE( qgsgeometry_cast( polySurfaceGeom.constGet() )->patchN( 0 )->numInteriorRings(), 1 ); // unchanged +} + QGSTEST_MAIN( TestQgsPolyhedralSurface ) #include "testqgspolyhedralsurface.moc" diff --git a/tests/src/core/geometry/testqgstriangulatedsurface.cpp b/tests/src/core/geometry/testqgstriangulatedsurface.cpp index 82d2bb42a47c..1ca220fc9eeb 100644 --- a/tests/src/core/geometry/testqgstriangulatedsurface.cpp +++ b/tests/src/core/geometry/testqgstriangulatedsurface.cpp @@ -12,6 +12,7 @@ * (at your option) any later version. * * * ***************************************************************************/ +#include "qgsgeometry.h" #include "qgslinestring.h" #include "qgspolygon.h" #include "qgssurface.h" @@ -58,6 +59,8 @@ class TestQgsTriangulatedSurface : public QObject void testWKT(); void testExport(); void testCast(); + void testCoerceToTypeErrorMessage(); + void testGeometryEditUtilsAddPart(); }; void TestQgsTriangulatedSurface::testConstructor() @@ -1125,6 +1128,74 @@ void TestQgsTriangulatedSurface::testCast() QVERIFY( !pCast2.fromWkt( u"TINZ((111111))"_s ) ); } +void TestQgsTriangulatedSurface::testCoerceToTypeErrorMessage() +{ + // Test Phase 1: coerceToType should return error message when polygon has too many vertices for TIN + + // Create a polygon with 4 vertices (5 points including closing) - too many for a triangle + QgsPolygon polygon; + QgsLineString *exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 1, 0 ) << QgsPoint( 1, 1 ) << QgsPoint( 0, 1 ) << QgsPoint( 0, 0 ) ); + polygon.setExteriorRing( exteriorRing ); + QgsGeometry geom( polygon.clone() ); + + QVector result = geom.coerceToType( Qgis::WkbType::TIN, 0, 0, true ); + + // Should fail with empty result + QVERIFY( result.isEmpty() ); + + // Test with a valid triangle (3 vertices = 4 points including closing) + QgsPolygon trianglePolygon; + QgsLineString *triangleRing = new QgsLineString(); + triangleRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 1, 0 ) << QgsPoint( 0.5, 1 ) << QgsPoint( 0, 0 ) ); + trianglePolygon.setExteriorRing( triangleRing ); + QgsGeometry triangleGeom( trianglePolygon.clone() ); + + result = triangleGeom.coerceToType( Qgis::WkbType::TIN, 0, 0, true ); + + // Should succeed + QVERIFY( !result.isEmpty() ); +} + +void TestQgsTriangulatedSurface::testGeometryEditUtilsAddPart() +{ + // Test Phase 2: QgsGeometry::addPartV2 for TIN + + // Create an empty TIN geometry + QgsGeometry tinGeom( std::make_unique() ); + QCOMPARE( qgsgeometry_cast( tinGeom.constGet() )->numPatches(), 0 ); + + // Add a triangle + QgsTriangle triangle1( QgsPoint( 0, 0 ), QgsPoint( 1, 0 ), QgsPoint( 0.5, 1 ) ); + Qgis::GeometryOperationResult result = tinGeom.addPartV2( triangle1.clone(), Qgis::WkbType::TIN ); + QCOMPARE( result, Qgis::GeometryOperationResult::Success ); + QCOMPARE( qgsgeometry_cast( tinGeom.constGet() )->numPatches(), 1 ); + + // Add another triangle + QgsTriangle triangle2( QgsPoint( 1, 0 ), QgsPoint( 2, 0 ), QgsPoint( 1.5, 1 ) ); + result = tinGeom.addPartV2( triangle2.clone(), Qgis::WkbType::TIN ); + QCOMPARE( result, Qgis::GeometryOperationResult::Success ); + QCOMPARE( qgsgeometry_cast( tinGeom.constGet() )->numPatches(), 2 ); + + // Try to add a polygon that is a valid triangle (3 vertices) + QgsPolygon validPolygon; + QgsLineString *ring = new QgsLineString(); + ring->setPoints( QgsPointSequence() << QgsPoint( 2, 0 ) << QgsPoint( 3, 0 ) << QgsPoint( 2.5, 1 ) << QgsPoint( 2, 0 ) ); + validPolygon.setExteriorRing( ring ); + result = tinGeom.addPartV2( validPolygon.clone(), Qgis::WkbType::TIN ); + QCOMPARE( result, Qgis::GeometryOperationResult::Success ); + QCOMPARE( qgsgeometry_cast( tinGeom.constGet() )->numPatches(), 3 ); + + // Try to add a polygon that is NOT a triangle (4 vertices) - should fail + QgsPolygon invalidPolygon; + ring = new QgsLineString(); + ring->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 1, 0 ) << QgsPoint( 1, 1 ) << QgsPoint( 0, 1 ) << QgsPoint( 0, 0 ) ); + invalidPolygon.setExteriorRing( ring ); + result = tinGeom.addPartV2( invalidPolygon.clone(), Qgis::WkbType::TIN ); + QCOMPARE( result, Qgis::GeometryOperationResult::InvalidInputGeometryType ); + QCOMPARE( qgsgeometry_cast( tinGeom.constGet() )->numPatches(), 3 ); // unchanged +} + QGSTEST_MAIN( TestQgsTriangulatedSurface ) #include "testqgstriangulatedsurface.moc" diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index 0e77d553e04f..09c999238883 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -12488,6 +12488,96 @@ def coerce_to_wkt( ["PolyhedralSurface Z (((1 1 2, 1 2 2, 2 2 3, 5 5 3, 1 1 2)))"], ) + # Polygon/Triangle to TIN + self.assertEqual( + coerce_to_wkt( + "Polygon ((1 1, 1 2, 2 2, 1 1))", + QgsWkbTypes.Type.TIN, + ), + ["TIN (((1 1, 1 2, 2 2, 1 1)))"], + ) + self.assertEqual( + coerce_to_wkt( + "Polygon Z((1 1 0, 1 2 0, 2 2 0, 1 1 0))", + QgsWkbTypes.Type.TINZ, + ), + ["TIN Z (((1 1 0, 1 2 0, 2 2 0, 1 1 0)))"], + ) + self.assertEqual( + coerce_to_wkt( + "Triangle ((1 1, 1 2, 2 2, 1 1))", + QgsWkbTypes.Type.TIN, + ), + ["TIN (((1 1, 1 2, 2 2, 1 1)))"], + ) + self.assertEqual( + coerce_to_wkt( + "MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))", + QgsWkbTypes.Type.TIN, + ), + ["TIN (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))"], + ) + + # PolyhedralSurface to MultiPolygon + self.assertEqual( + coerce_to_wkt( + "PolyhedralSurface (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))", + QgsWkbTypes.Type.MultiPolygon, + ), + ["MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))"], + ) + self.assertEqual( + coerce_to_wkt( + "PolyhedralSurface Z (((1 1 0, 1 2 0, 2 2 0, 1 1 0)),((3 3 0, 4 3 0, 4 4 0, 3 3 0)))", + QgsWkbTypes.Type.MultiPolygonZ, + ), + [ + "MultiPolygon Z (((1 1 0, 1 2 0, 2 2 0, 1 1 0)),((3 3 0, 4 3 0, 4 4 0, 3 3 0)))" + ], + ) + # PolyhedralSurface to Polygon (single patch) + self.assertEqual( + coerce_to_wkt( + "PolyhedralSurface (((1 1, 1 2, 2 2, 1 1)))", + QgsWkbTypes.Type.Polygon, + ), + ["Polygon ((1 1, 1 2, 2 2, 1 1))"], + ) + # PolyhedralSurface to Polygon (multi patches -> multiple results) + self.assertEqual( + coerce_to_wkt( + "PolyhedralSurface (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))", + QgsWkbTypes.Type.Polygon, + ), + ["Polygon ((1 1, 1 2, 2 2, 1 1))", "Polygon ((3 3, 4 3, 4 4, 3 3))"], + ) + + # TIN to MultiPolygon + self.assertEqual( + coerce_to_wkt( + "TIN (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))", + QgsWkbTypes.Type.MultiPolygon, + ), + ["MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))"], + ) + self.assertEqual( + coerce_to_wkt( + "TIN Z (((1 1 0, 1 2 0, 2 2 0, 1 1 0)),((3 3 0, 4 3 0, 4 4 0, 3 3 0)))", + QgsWkbTypes.Type.MultiPolygonZ, + ), + [ + "MultiPolygon Z (((1 1 0, 1 2 0, 2 2 0, 1 1 0)),((3 3 0, 4 3 0, 4 4 0, 3 3 0)))" + ], + ) + # TIN to Polygon (single patch) + self.assertEqual( + coerce_to_wkt( + "TIN (((1 1, 1 2, 2 2, 1 1)))", + QgsWkbTypes.Type.Polygon, + ), + ["Polygon ((1 1, 1 2, 2 2, 1 1))"], + ) + # GeometryCollection of Point to MultiPoint self.assertEqual( coerce_to_wkt(