diff --git a/CMakeLists.txt b/CMakeLists.txt index 345d96c85e0..74759935236 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1101,7 +1101,7 @@ if (WITH_CORE AND WITH_BINDINGS) include(SIPMacros) set(SIP_INCLUDES ${PYQT_SIP_DIR} ${CMAKE_SOURCE_DIR}/python) - set(SIP_CONCAT_PARTS 22) + set(SIP_CONCAT_PARTS 24) if (NOT BINDINGS_GLOBAL_INSTALL) set(Python_SITEARCH ${QGIS_DATA_DIR}/python) diff --git a/python/PyQt6/core/auto_additions/qgspolyhedralsurface.py b/python/PyQt6/core/auto_additions/qgspolyhedralsurface.py new file mode 100644 index 00000000000..a3a74854ddf --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgspolyhedralsurface.py @@ -0,0 +1,5 @@ +# The following has been generated automatically from src/core/geometry/qgspolyhedralsurface.h +try: + QgsPolyhedralSurface.__group__ = ['geometry'] +except NameError: + pass diff --git a/python/PyQt6/core/auto_generated/geometry/qgspolyhedralsurface.sip.in b/python/PyQt6/core/auto_generated/geometry/qgspolyhedralsurface.sip.in new file mode 100644 index 00000000000..e86c76dfbcf --- /dev/null +++ b/python/PyQt6/core/auto_generated/geometry/qgspolyhedralsurface.sip.in @@ -0,0 +1,302 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgspolyhedralsurface.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsPolyhedralSurface: QgsSurface +{ +%Docstring(signature="appended") +Polyhedral surface geometry type. + +A polyhedral surface is a collection of polygons which share common boundary segments. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgspolyhedralsurface.h" +%End + public: + QgsPolyhedralSurface(); + QgsPolyhedralSurface( const QgsPolyhedralSurface &p ); + + + QgsPolyhedralSurface( const QgsMultiPolygon *multiPolygon ); +%Docstring +Creates a polyhedral surface from a multiPolygon. +%End + + + + public: + virtual bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + virtual bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + + virtual bool operator==( const QgsAbstractGeometry &other ) const; + + virtual bool operator!=( const QgsAbstractGeometry &other ) const; + + ~QgsPolyhedralSurface(); + + virtual QString geometryType() const /HoldGIL/; + + int dimension() const final /HoldGIL/; + virtual QgsPolyhedralSurface *clone() const /Factory/; + + virtual void clear(); + + + virtual bool fromWkb( QgsConstWkbPtr &wkb ); + + virtual bool fromWkt( const QString &wkt ); + + + virtual int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QString asWkt( int precision = 17 ) const; + + virtual QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QString asKml( int precision = 17 ) const; + + virtual void normalize() /HoldGIL/; + + + virtual double area() const /HoldGIL/; + + virtual double perimeter() const /HoldGIL/; + + virtual QgsAbstractGeometry *boundary() const /Factory/; + + virtual QgsPolyhedralSurface *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; + + virtual QgsPolyhedralSurface *simplifyByDistance( double tolerance ) const /Factory/; + + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + + virtual bool boundingBoxIntersects( const QgsBox3D &box3d ) const /HoldGIL/; + + + int numPatches() const /HoldGIL/; +%Docstring +Returns the number of patches contained with the polyhedral surface. + +.. seealso:: :py:func:`patchN` +%End + + + SIP_PYOBJECT patchN( int i ) /HoldGIL,TypeHint="QgsPolygon"/; +%Docstring +Retrieves a patch from the polyhedral surface. The first patch has index 0. + +:raises IndexError: if no patch with the specified index exists. + +.. seealso:: :py:func:`numPatches` +%End +%MethodCode + if ( a0 < 0 || a0 >= sipCpp->numPatches() ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + return sipConvertFromType( const_cast< QgsPolygon * >( sipCpp->patchN( a0 ) ), sipType_QgsPolygon, NULL ); + } +%End + + virtual void setPatches( const QVector &patches /Transfer/ ); +%Docstring +Sets all patches, transferring ownership to the polyhedral surface. +%End + + virtual void addPatch( QgsPolygon *patch /Transfer/ ); +%Docstring +Adds a patch to the geometry, transferring ownership to the polyhedral surface. +%End + + + bool removePatch( int ringIndex ); +%Docstring +Removes a patch from the polyhedral surface. The first patch has index 0. +The corresponding patch is removed from the polyhedral surface and deleted. + +:raises IndexError: if no patch with the specified index exists. +%End +%MethodCode + if ( a0 < 0 || a0 >= sipCpp->numPatches() ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + return PyBool_FromLong( sipCpp->removePatch( a0 ) ); + } +%End + + virtual QPainterPath asQPainterPath() const; + + virtual void draw( QPainter &p ) const; + + virtual void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ) throw( QgsCsException ); + + virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ); + + + virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex ); + + virtual bool moveVertex( QgsVertexId position, const QgsPoint &newPos ); + + virtual bool deleteVertex( QgsVertexId position ); + + + virtual QgsCoordinateSequence coordinateSequence() const; + + virtual int nCoordinates() const; + + virtual int vertexNumberFromVertexId( QgsVertexId id ) const; + + virtual bool isEmpty() const /HoldGIL/; + + virtual double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt /Out/, QgsVertexId &vertexAfter /Out/, int *leftOf /Out/ = 0, double epsilon = 4 * DBL_EPSILON ) const; + + + virtual bool nextVertex( QgsVertexId &id, QgsPoint &vertex /Out/ ) const; + + virtual void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex /Out/, QgsVertexId &nextVertex /Out/ ) const; + + bool hasCurvedSegments() const final; + + virtual QgsAbstractGeometry *segmentize( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/; + +%Docstring +Returns a geometry without curves. Caller takes ownership + +:param tolerance: segmentation tolerance +:param toleranceType: maximum segmentation angle or maximum difference between approximation and curve +%End + + virtual double vertexAngle( QgsVertexId vertex ) const; + +%Docstring +Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments. + +:param vertex: the vertex id + +:return: rotation in radians, clockwise from north +%End + + virtual int vertexCount( int part = 0, int ring = 0 ) const; + + virtual int ringCount( int part = 0 ) const /HoldGIL/; + + virtual int partCount() const /HoldGIL/; + + virtual QgsPoint vertexAt( QgsVertexId id ) const; + + virtual double segmentLength( QgsVertexId startVertex ) const; + + + virtual bool addZValue( double zValue = 0 ); + + virtual bool addMValue( double mValue = 0 ); + + virtual bool dropZValue(); + + virtual bool dropMValue(); + + virtual void swapXy(); + + + virtual QgsMultiSurface *toCurveType() const /Factory/; + + + virtual bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = 0 ); + + + QgsMultiPolygon *toMultiPolygon() const /Factory/; +%Docstring +Converts a polyhedral surface to a multipolygon. +Caller takes ownership. +%End + + + virtual QgsPolyhedralSurface *createEmptyWithSameType() const /Factory/; + + + SIP_PYOBJECT __repr__(); +%MethodCode + QString wkt = sipCpp->asWkt(); + if ( wkt.length() > 1000 ) + wkt = wkt.left( 1000 ) + QStringLiteral( "..." ); + QString str = QStringLiteral( "" ).arg( wkt ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + + Py_ssize_t __len__() const; +%Docstring +Returns the number of patches within the polyhedral surface. +%End +%MethodCode + sipRes = sipCpp->numPatches(); +%End + + SIP_PYOBJECT __getitem__( int index ) /TypeHint="QgsPolygon"/; +%Docstring +Returns the geometry at the specified ``index``. + +Indexes can be less than 0, in which case they correspond to geometries from the end of the collect. E.g. an index of -1 +corresponds to the last geometry in the collection. + +:raises IndexError: if no geometry with the specified ``index`` exists. +%End +%MethodCode + const int count = sipCpp->numPatches(); + if ( a0 < -count || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else if ( a0 >= 0 ) + { + return sipConvertFromType( sipCpp->patchN( a0 ), sipType_QgsPolygon, NULL ); + } + else + { + return sipConvertFromType( sipCpp->patchN( count + a0 ), sipType_QgsPolygon, NULL ); + } +%End + + protected: + + virtual int childCount() const; + + virtual QgsAbstractGeometry *childGeometry( int index ) const; + + virtual int compareToSameClass( const QgsAbstractGeometry *other ) const; + + virtual QgsBox3D calculateBoundingBox3D() const; + + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgspolyhedralsurface.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index 46a81dd90d7..254e63caa25 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -353,6 +353,7 @@ %Include auto_generated/geometry/qgsorientedbox3d.sip %Include auto_generated/geometry/qgspoint.sip %Include auto_generated/geometry/qgspolygon.sip +%Include auto_generated/geometry/qgspolyhedralsurface.sip %Include auto_generated/geometry/qgsquadrilateral.sip %Include auto_generated/geometry/qgsrectangle.sip %Include auto_generated/geometry/qgsreferencedgeometry.sip diff --git a/python/core/auto_additions/qgspolyhedralsurface.py b/python/core/auto_additions/qgspolyhedralsurface.py new file mode 100644 index 00000000000..a3a74854ddf --- /dev/null +++ b/python/core/auto_additions/qgspolyhedralsurface.py @@ -0,0 +1,5 @@ +# The following has been generated automatically from src/core/geometry/qgspolyhedralsurface.h +try: + QgsPolyhedralSurface.__group__ = ['geometry'] +except NameError: + pass diff --git a/python/core/auto_generated/geometry/qgspolyhedralsurface.sip.in b/python/core/auto_generated/geometry/qgspolyhedralsurface.sip.in new file mode 100644 index 00000000000..73a81c924cd --- /dev/null +++ b/python/core/auto_generated/geometry/qgspolyhedralsurface.sip.in @@ -0,0 +1,302 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgspolyhedralsurface.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + + +class QgsPolyhedralSurface: QgsSurface +{ +%Docstring(signature="appended") +Polyhedral surface geometry type. + +A polyhedral surface is a collection of polygons which share common boundary segments. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgspolyhedralsurface.h" +%End + public: + QgsPolyhedralSurface(); + QgsPolyhedralSurface( const QgsPolyhedralSurface &p ); + + + QgsPolyhedralSurface( const QgsMultiPolygon *multiPolygon ); +%Docstring +Creates a polyhedral surface from a multiPolygon. +%End + + + + public: + virtual bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + virtual bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/; + + virtual bool operator==( const QgsAbstractGeometry &other ) const; + + virtual bool operator!=( const QgsAbstractGeometry &other ) const; + + ~QgsPolyhedralSurface(); + + virtual QString geometryType() const /HoldGIL/; + + int dimension() const final /HoldGIL/; + virtual QgsPolyhedralSurface *clone() const /Factory/; + + virtual void clear(); + + + virtual bool fromWkb( QgsConstWkbPtr &wkb ); + + virtual bool fromWkt( const QString &wkt ); + + + virtual int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const; + + virtual QString asWkt( int precision = 17 ) const; + + virtual QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const; + + virtual QString asKml( int precision = 17 ) const; + + virtual void normalize() /HoldGIL/; + + + virtual double area() const /HoldGIL/; + + virtual double perimeter() const /HoldGIL/; + + virtual QgsAbstractGeometry *boundary() const /Factory/; + + virtual QgsPolyhedralSurface *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; + + virtual QgsPolyhedralSurface *simplifyByDistance( double tolerance ) const /Factory/; + + virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); + + virtual bool boundingBoxIntersects( const QgsBox3D &box3d ) const /HoldGIL/; + + + int numPatches() const /HoldGIL/; +%Docstring +Returns the number of patches contained with the polyhedral surface. + +.. seealso:: :py:func:`patchN` +%End + + + SIP_PYOBJECT patchN( int i ) /HoldGIL,TypeHint="QgsPolygon"/; +%Docstring +Retrieves a patch from the polyhedral surface. The first patch has index 0. + +:raises IndexError: if no patch with the specified index exists. + +.. seealso:: :py:func:`numPatches` +%End +%MethodCode + if ( a0 < 0 || a0 >= sipCpp->numPatches() ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + return sipConvertFromType( const_cast< QgsPolygon * >( sipCpp->patchN( a0 ) ), sipType_QgsPolygon, NULL ); + } +%End + + virtual void setPatches( const QVector &patches /Transfer/ ); +%Docstring +Sets all patches, transferring ownership to the polyhedral surface. +%End + + virtual void addPatch( QgsPolygon *patch /Transfer/ ); +%Docstring +Adds a patch to the geometry, transferring ownership to the polyhedral surface. +%End + + + bool removePatch( int ringIndex ); +%Docstring +Removes a patch from the polyhedral surface. The first patch has index 0. +The corresponding patch is removed from the polyhedral surface and deleted. + +:raises IndexError: if no patch with the specified index exists. +%End +%MethodCode + if ( a0 < 0 || a0 >= sipCpp->numPatches() ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + return PyBool_FromLong( sipCpp->removePatch( a0 ) ); + } +%End + + virtual QPainterPath asQPainterPath() const; + + virtual void draw( QPainter &p ) const; + + virtual void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ) throw( QgsCsException ); + + virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ); + + + virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex ); + + virtual bool moveVertex( QgsVertexId position, const QgsPoint &newPos ); + + virtual bool deleteVertex( QgsVertexId position ); + + + virtual QgsCoordinateSequence coordinateSequence() const; + + virtual int nCoordinates() const; + + virtual int vertexNumberFromVertexId( QgsVertexId id ) const; + + virtual bool isEmpty() const /HoldGIL/; + + virtual double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt /Out/, QgsVertexId &vertexAfter /Out/, int *leftOf /Out/ = 0, double epsilon = 4 * DBL_EPSILON ) const; + + + virtual bool nextVertex( QgsVertexId &id, QgsPoint &vertex /Out/ ) const; + + virtual void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex /Out/, QgsVertexId &nextVertex /Out/ ) const; + + bool hasCurvedSegments() const final; + + virtual QgsAbstractGeometry *segmentize( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/; + +%Docstring +Returns a geometry without curves. Caller takes ownership + +:param tolerance: segmentation tolerance +:param toleranceType: maximum segmentation angle or maximum difference between approximation and curve +%End + + virtual double vertexAngle( QgsVertexId vertex ) const; + +%Docstring +Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments. + +:param vertex: the vertex id + +:return: rotation in radians, clockwise from north +%End + + virtual int vertexCount( int part = 0, int ring = 0 ) const; + + virtual int ringCount( int part = 0 ) const /HoldGIL/; + + virtual int partCount() const /HoldGIL/; + + virtual QgsPoint vertexAt( QgsVertexId id ) const; + + virtual double segmentLength( QgsVertexId startVertex ) const; + + + virtual bool addZValue( double zValue = 0 ); + + virtual bool addMValue( double mValue = 0 ); + + virtual bool dropZValue(); + + virtual bool dropMValue(); + + virtual void swapXy(); + + + virtual QgsMultiSurface *toCurveType() const /Factory/; + + + virtual bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = 0 ); + + + QgsMultiPolygon *toMultiPolygon() const /Factory/; +%Docstring +Converts a polyhedral surface to a multipolygon. +Caller takes ownership. +%End + + + virtual QgsPolyhedralSurface *createEmptyWithSameType() const /Factory/; + + + SIP_PYOBJECT __repr__(); +%MethodCode + QString wkt = sipCpp->asWkt(); + if ( wkt.length() > 1000 ) + wkt = wkt.left( 1000 ) + QStringLiteral( "..." ); + QString str = QStringLiteral( "" ).arg( wkt ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + + int __len__() const; +%Docstring +Returns the number of patches within the polyhedral surface. +%End +%MethodCode + sipRes = sipCpp->numPatches(); +%End + + SIP_PYOBJECT __getitem__( int index ) /TypeHint="QgsPolygon"/; +%Docstring +Returns the geometry at the specified ``index``. + +Indexes can be less than 0, in which case they correspond to geometries from the end of the collect. E.g. an index of -1 +corresponds to the last geometry in the collection. + +:raises IndexError: if no geometry with the specified ``index`` exists. +%End +%MethodCode + const int count = sipCpp->numPatches(); + if ( a0 < -count || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else if ( a0 >= 0 ) + { + return sipConvertFromType( sipCpp->patchN( a0 ), sipType_QgsPolygon, NULL ); + } + else + { + return sipConvertFromType( sipCpp->patchN( count + a0 ), sipType_QgsPolygon, NULL ); + } +%End + + protected: + + virtual int childCount() const; + + virtual QgsAbstractGeometry *childGeometry( int index ) const; + + virtual int compareToSameClass( const QgsAbstractGeometry *other ) const; + + virtual QgsBox3D calculateBoundingBox3D() const; + + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/geometry/qgspolyhedralsurface.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 46a81dd90d7..254e63caa25 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -353,6 +353,7 @@ %Include auto_generated/geometry/qgsorientedbox3d.sip %Include auto_generated/geometry/qgspoint.sip %Include auto_generated/geometry/qgspolygon.sip +%Include auto_generated/geometry/qgspolyhedralsurface.sip %Include auto_generated/geometry/qgsquadrilateral.sip %Include auto_generated/geometry/qgsrectangle.sip %Include auto_generated/geometry/qgsreferencedgeometry.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c5f7f6d6936..5e96f47377c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -902,6 +902,7 @@ set(QGIS_CORE_SRCS geometry/qgsorientedbox3d.cpp geometry/qgspoint.cpp geometry/qgspolygon.cpp + geometry/qgspolyhedralsurface.cpp geometry/qgsquadrilateral.cpp geometry/qgsrectangle.cpp geometry/qgsreferencedgeometry.cpp @@ -1491,6 +1492,7 @@ set(QGIS_CORE_HDRS geometry/qgsorientedbox3d.h geometry/qgspoint.h geometry/qgspolygon.h + geometry/qgspolyhedralsurface.h geometry/qgsquadrilateral.h geometry/qgsrectangle.h geometry/qgsreferencedgeometry.h diff --git a/src/core/geometry/qgspolyhedralsurface.cpp b/src/core/geometry/qgspolyhedralsurface.cpp new file mode 100644 index 00000000000..76c4a9eba5f --- /dev/null +++ b/src/core/geometry/qgspolyhedralsurface.cpp @@ -0,0 +1,996 @@ +/*************************************************************************** + qgspolyhedralsurface.cpp + --------------------- + begin : August 2024 + copyright : (C) 2024 by Jean Felder + email : jean dot felder at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgspolyhedralsurface.h" +#include "qgsapplication.h" +#include "qgscurve.h" +#include "qgsfeedback.h" +#include "qgsgeometryutils.h" +#include "qgslinestring.h" +#include "qgslogger.h" +#include "qgsmultipolygon.h" +#include "qgsmultisurface.h" +#include "qgspolygon.h" +#include "qgsvertexid.h" +#include "qgswkbptr.h" +#include "qgsmultilinestring.h" + +#include +#include +#include +#include + +QgsPolyhedralSurface::QgsPolyhedralSurface() +{ + mWkbType = Qgis::WkbType::PolyhedralSurface; +} + +QgsPolyhedralSurface::QgsPolyhedralSurface( const QgsMultiPolygon *multiPolygon ) +{ + if ( multiPolygon->isEmpty() ) + { + return; + } + + mPatches.reserve( multiPolygon->numGeometries() ); + for ( int i = 0; i < multiPolygon->numGeometries(); ++i ) + { + mPatches.append( multiPolygon->polygonN( i )->clone() ); + } + + setZMTypeFromSubGeometry( multiPolygon->polygonN( 0 ), Qgis::WkbType::PolyhedralSurface ); +} + +QgsPolyhedralSurface::~QgsPolyhedralSurface() +{ + QgsPolyhedralSurface::clear(); +} + +QgsPolyhedralSurface *QgsPolyhedralSurface::createEmptyWithSameType() const +{ + auto result = std::make_unique< QgsPolyhedralSurface >(); + result->mWkbType = mWkbType; + return result.release(); +} + +QString QgsPolyhedralSurface::geometryType() const +{ + return QStringLiteral( "PolyhedralSurface" ); +} + +int QgsPolyhedralSurface::dimension() const +{ + return 2; +} + +QgsPolyhedralSurface::QgsPolyhedralSurface( const QgsPolyhedralSurface &p ) + : QgsSurface( p ) + +{ + mWkbType = p.mWkbType; + mPatches.reserve( p.mPatches.size() ); + + for ( const QgsPolygon *patch : p.mPatches ) + { + mPatches.push_back( patch->clone() ); + } + + mBoundingBox = p.mBoundingBox; + mHasCachedValidity = p.mHasCachedValidity; + mValidityFailureReason = p.mValidityFailureReason; +} + +// cppcheck-suppress operatorEqVarError +QgsPolyhedralSurface &QgsPolyhedralSurface::operator=( const QgsPolyhedralSurface &p ) +{ + if ( &p != this ) + { + QgsSurface::operator=( p ); + mPatches.reserve( p.mPatches.size() ); + + for ( const QgsPolygon *patch : p.mPatches ) + { + mPatches.push_back( patch->clone() ); + } + } + return *this; +} + +QgsPolyhedralSurface *QgsPolyhedralSurface::clone() const +{ + return new QgsPolyhedralSurface( *this ); +} + +void QgsPolyhedralSurface::clear() +{ + mWkbType = Qgis::WkbType::PolyhedralSurface; + qDeleteAll( mPatches ); + mPatches.clear(); + clearCache(); +} + + +bool QgsPolyhedralSurface::fromWkb( QgsConstWkbPtr &wkbPtr ) +{ + clear(); + + if ( !wkbPtr ) + { + return false; + } + + Qgis::WkbType type = wkbPtr.readHeader(); + if ( QgsWkbTypes::flatType( type ) != Qgis::WkbType::PolyhedralSurface ) + { + return false; + } + mWkbType = type; + + int nPatches; + wkbPtr >> nPatches; + std::unique_ptr< QgsPolygon > currentPatch; + for ( int i = 0; i < nPatches; ++i ) + { + Qgis::WkbType polygonType = wkbPtr.readHeader(); + wkbPtr -= 1 + sizeof( int ); + Qgis::WkbType flatPolygonType = QgsWkbTypes::flatType( polygonType ); + if ( flatPolygonType == Qgis::WkbType::Polygon ) + { + currentPatch.reset( new QgsPolygon() ); + } + else + { + return false; + } + currentPatch->fromWkb( wkbPtr ); // also updates wkbPtr + mPatches.append( currentPatch.release() ); + } + + return true; +} + +bool QgsPolyhedralSurface::fromWkt( const QString &wkt ) +{ + clear(); + + QPair parts = QgsGeometryUtils::wktReadBlock( wkt ); + + if ( QgsWkbTypes::geometryType( parts.first ) != Qgis::GeometryType::Polygon ) + return false; + + mWkbType = parts.first; + + QString secondWithoutParentheses = parts.second; + secondWithoutParentheses = secondWithoutParentheses.remove( '(' ).remove( ')' ).simplified().remove( ' ' ); + if ( ( parts.second.compare( QLatin1String( "EMPTY" ), Qt::CaseInsensitive ) == 0 ) || + secondWithoutParentheses.isEmpty() ) + return true; + + QString defaultChildWkbType = QStringLiteral( "Polygon%1%2" ).arg( is3D() ? QStringLiteral( "Z" ) : QString(), isMeasure() ? QStringLiteral( "M" ) : QString() ); + + const QStringList blocks = QgsGeometryUtils::wktGetChildBlocks( parts.second, defaultChildWkbType ); + for ( const QString &childWkt : blocks ) + { + QPair childParts = QgsGeometryUtils::wktReadBlock( childWkt ); + + if ( QgsWkbTypes::flatType( childParts.first ) == Qgis::WkbType::Polygon ) + { + mPatches.append( new QgsPolygon() ); + } + else + { + clear(); + return false; + } + + if ( !mPatches.back()->fromWkt( childWkt ) ) + { + clear(); + return false; + } + } + + return true; +} + +QgsBox3D QgsPolyhedralSurface::calculateBoundingBox3D() const +{ + if ( mPatches.empty() ) + { + return QgsBox3D(); + } + + QgsBox3D bbox = mPatches.at( 0 )->boundingBox3D(); + for ( int i = 1; i < mPatches.size(); ++i ) + { + QgsBox3D polygonBox = mPatches.at( i )->boundingBox3D(); + bbox.combineWith( polygonBox ); + } + return bbox; +} + +int QgsPolyhedralSurface::wkbSize( QgsAbstractGeometry::WkbFlags flags ) const +{ + int binarySize = sizeof( char ) + sizeof( quint32 ) + sizeof( quint32 ); + for ( const QgsPolygon *patch : mPatches ) + { + binarySize += patch->wkbSize( flags ); + } + return binarySize; +} + +QByteArray QgsPolyhedralSurface::asWkb( WkbFlags flags ) const +{ + QByteArray wkbArray; + wkbArray.resize( QgsPolyhedralSurface::wkbSize( flags ) ); + QgsWkbPtr wkb( wkbArray ); + wkb << static_cast( QgsApplication::endian() ); + wkb << static_cast( wkbType() ); + wkb << static_cast( mPatches.size() ); + for ( const QgsPolygon *patch : mPatches ) + { + wkb << patch->asWkb( flags ); + } + return wkbArray; +} + +QString QgsPolyhedralSurface::asWkt( int precision ) const +{ + QString wkt = wktTypeStr(); + + if ( isEmpty() ) + wkt += QLatin1String( " EMPTY" ); + else + { + wkt += QLatin1String( " (" ); + for ( const QgsPolygon *patch : mPatches ) + { + QString childWkt = patch->asWkt( precision ); + if ( qgsgeometry_cast( patch ) ) + { + // Type names of linear geometries are omitted + childWkt = childWkt.mid( childWkt.indexOf( '(' ) ); + } + wkt += childWkt + ','; + } + if ( wkt.endsWith( ',' ) ) + { + wkt.chop( 1 ); // Remove last ',' + } + wkt += ')'; + } + return wkt; +} + +QDomElement QgsPolyhedralSurface::asGml2( QDomDocument &, int, const QString &, const AxisOrder ) const +{ + QgsDebugError( QStringLiteral( "gml version 2 does not support PolyhedralSurface geometry" ) ); + return QDomElement(); +} + +QDomElement QgsPolyhedralSurface::asGml3( QDomDocument &doc, int precision, const QString &ns, const QgsAbstractGeometry::AxisOrder axisOrder ) const +{ + QDomElement elemPolyhedralSurface = doc.createElementNS( ns, QStringLiteral( "PolyhedralSurface" ) ); + + if ( isEmpty() ) + return elemPolyhedralSurface; + + QDomElement elemPolygonPatches = doc.createElementNS( ns, QStringLiteral( "polygonPatches" ) ); + + for ( QgsPolygon *patch : mPatches ) + { + QDomElement elemPolygonPatch = patch->asGml3( doc, precision, ns, axisOrder ); + elemPolygonPatch.setTagName( "PolygonPatch" ); + + elemPolygonPatches.appendChild( elemPolygonPatch ); + } + elemPolyhedralSurface.appendChild( elemPolygonPatches ); + + return elemPolyhedralSurface; +} + +json QgsPolyhedralSurface::asJsonObject( int precision ) const +{ + // GeoJSON format does not support PolyhedralSurface geometry + // Return a multipolygon instead; + std::unique_ptr multiPolygon( toMultiPolygon() ); + return multiPolygon->asJsonObject( precision ); +} + +QString QgsPolyhedralSurface::asKml( int ) const +{ + QgsDebugError( QStringLiteral( "kml format does not support PolyhedralSurface geometry" ) ); + return QString( "" ); +} + +void QgsPolyhedralSurface::normalize() +{ + for ( QgsPolygon *patch : mPatches ) + { + QgsCurve *exteriorRing = patch->exteriorRing(); + if ( !exteriorRing ) + continue; + + exteriorRing->normalize(); + + if ( patch->numInteriorRings() > 0 ) + { + QVector interiorRings; + for ( int i = 0, n = patch->numInteriorRings(); i < n; ++i ) + { + interiorRings.push_back( patch->interiorRing( i )->clone() ); + } + + // sort rings + std::sort( interiorRings.begin(), interiorRings.end(), []( const QgsCurve * a, const QgsCurve * b ) + { + return a->compareTo( b ) > 0; + } ); + + patch->removeInteriorRings(); + for ( QgsCurve *curve : interiorRings ) + patch->addInteriorRing( curve ); + } + } +} + +double QgsPolyhedralSurface::area() const +{ + // sum area of patches + double area = 0.0; + for ( const QgsPolygon *patch : mPatches ) + { + area += patch->area(); + } + + return area; +} + +double QgsPolyhedralSurface::perimeter() const +{ + // sum perimeter of patches + double perimeter = 0.0; + for ( const QgsPolygon *patch : mPatches ) + { + perimeter += patch->perimeter(); + } + + return perimeter; +} + +QgsAbstractGeometry *QgsPolyhedralSurface::boundary() const +{ + std::unique_ptr< QgsMultiLineString > multiLine( new QgsMultiLineString() ); + multiLine->reserve( mPatches.size() ); + for ( QgsPolygon *polygon : mPatches ) + { + std::unique_ptr polygonBoundary( polygon->boundary() ); + if ( QgsLineString *lineStringBoundary = qgsgeometry_cast< QgsLineString * >( polygonBoundary.get() ) ) + { + multiLine->addGeometry( lineStringBoundary->clone() ); + } + else if ( QgsMultiLineString *multiLineStringBoundary = qgsgeometry_cast< QgsMultiLineString * >( polygonBoundary.get() ) ) + { + for ( int j = 0; j < multiLineStringBoundary->numGeometries(); ++j ) + { + multiLine->addGeometry( multiLineStringBoundary->geometryN( j )->clone() ); + } + } + } + + if ( multiLine->numGeometries() == 0 ) + { + return nullptr; + } + return multiLine.release(); +} + +QgsPolyhedralSurface *QgsPolyhedralSurface::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const +{ + std::unique_ptr< QgsPolyhedralSurface > surface( createEmptyWithSameType() ); + + for ( QgsPolygon *patch : mPatches ) + { + // exterior ring + std::unique_ptr exteriorRing( static_cast< QgsCurve *>( patch->exteriorRing()->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing, removeRedundantPoints ) ) ); + if ( !exteriorRing ) + { + return nullptr; + } + + std::unique_ptr gridifiedPatch = std::make_unique(); + gridifiedPatch->setExteriorRing( exteriorRing.release() ); + + //interior rings + for ( int i = 0, n = patch->numInteriorRings(); i < n; ++i ) + { + QgsCurve *interiorRing = patch->interiorRing( i ); + if ( !interiorRing ) + continue; + + std::unique_ptr gridifiedInterior( static_cast< QgsCurve * >( interiorRing->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing, removeRedundantPoints ) ) ); + if ( gridifiedInterior ) + gridifiedPatch->addInteriorRing( gridifiedInterior.release() ); + } + + surface->addPatch( gridifiedPatch.release() ); + } + + return surface.release(); +} + +QgsPolyhedralSurface *QgsPolyhedralSurface::simplifyByDistance( double tolerance ) const +{ + if ( isEmpty() ) + return nullptr; + + std::unique_ptr< QgsPolyhedralSurface > simplifiedGeom = std::make_unique< QgsPolyhedralSurface >(); + for ( QgsPolygon *polygon : mPatches ) + { + std::unique_ptr polygonSimplified( polygon->simplifyByDistance( tolerance ) ); + simplifiedGeom->addPatch( polygonSimplified->surfaceToPolygon() ); + } + return simplifiedGeom.release(); +} + +bool QgsPolyhedralSurface::removeDuplicateNodes( double epsilon, bool useZValues ) +{ + bool result = false; + + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + if ( patch->removeDuplicateNodes( epsilon, useZValues ) ) + { + result = true; + } + } + return result; +} + +bool QgsPolyhedralSurface::boundingBoxIntersects( const QgsBox3D &box3d ) const +{ + // if we already have the bounding box calculated, then this check is trivial! + if ( !mBoundingBox.isNull() ) + { + return mBoundingBox.intersects( box3d ); + } + + // loop through each patch and test the bounding box intersection. + // This gives us a chance to use optimisations which may be present on the individual + // ring geometry subclasses, and at worst it will cause a calculation of the bounding box + // of each individual patch geometry which we would have to do anyway... (and these + // bounding boxes are cached, so would be reused without additional expense) + for ( const QgsPolygon *patch : mPatches ) + { + if ( patch->boundingBoxIntersects( box3d ) ) + return true; + } + + // even if we don't intersect the bounding box of any rings, we may still intersect the + // bounding box of the overall polygon (we are considering worst case scenario here and + // the polygon is invalid, with rings outside the exterior ring!) + // so here we fall back to the non-optimised base class check which has to first calculate + // the overall bounding box of the polygon.. + return QgsSurface::boundingBoxIntersects( box3d ); +} + +void QgsPolyhedralSurface::setPatches( const QVector &patches ) +{ + qDeleteAll( mPatches ); + mPatches.clear(); + + for ( QgsPolygon *patch : patches ) + { + addPatch( patch ); + } + + clearCache(); +} + +void QgsPolyhedralSurface::addPatch( QgsPolygon *patch ) +{ + if ( !patch ) + return; + + if ( mPatches.empty() ) + { + setZMTypeFromSubGeometry( patch, Qgis::WkbType::PolyhedralSurface ); + } + + // Ensure dimensionality of patch matches polyhedral surface + if ( !is3D() ) + patch->dropZValue(); + else if ( !patch->is3D() ) + patch->addZValue(); + + if ( !isMeasure() ) + patch->dropMValue(); + else if ( !patch->isMeasure() ) + patch->addMValue(); + + mPatches.append( patch ); + clearCache(); +} + +bool QgsPolyhedralSurface::removePatch( int patchIndex ) +{ + if ( patchIndex < 0 || patchIndex >= mPatches.size() ) + { + return false; + } + + delete mPatches.takeAt( patchIndex ); + clearCache(); + return true; +} + +QPainterPath QgsPolyhedralSurface::asQPainterPath() const +{ + QPainterPath painterPath; + for ( const QgsPolygon *patch : mPatches ) + { + QPainterPath patchPath = patch->asQPainterPath(); + patchPath.closeSubpath(); + painterPath.addPath( patchPath ); + } + + return painterPath; +} + +void QgsPolyhedralSurface::draw( QPainter &p ) const +{ + if ( mPatches.empty() ) + return; + + for ( const QgsPolygon *patch : mPatches ) + { + patch->draw( p ); + } +} + +void QgsPolyhedralSurface::transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d, bool transformZ ) +{ + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->transform( ct, d, transformZ ); + } + clearCache(); +} + +void QgsPolyhedralSurface::transform( const QTransform &t, double zTranslate, double zScale, double mTranslate, double mScale ) +{ + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->transform( t, zTranslate, zScale, mTranslate, mScale ); + } + clearCache(); +} + +QgsCoordinateSequence QgsPolyhedralSurface::coordinateSequence() const +{ + QgsCoordinateSequence sequence; + for ( const QgsPolygon *polygon : std::as_const( mPatches ) ) + { + QgsCoordinateSequence polyCoords = polygon->coordinateSequence(); + QgsCoordinateSequence::const_iterator cIt = polyCoords.constBegin(); + for ( ; cIt != polyCoords.constEnd(); ++cIt ) + { + sequence.push_back( *cIt ); + } + } + + return sequence; +} + +int QgsPolyhedralSurface::nCoordinates() const +{ + int count = 0; + for ( const QgsPolygon *patch : mPatches ) + { + count += patch->nCoordinates(); + } + return count; +} + +int QgsPolyhedralSurface::vertexNumberFromVertexId( QgsVertexId id ) const +{ + if ( id.part < 0 || id.part >= partCount() ) + return -1; + + int number = 0; + for ( int i = 0; i < mPatches.count(); ++i ) + { + if ( id.part == i ) + { + int partNumber = mPatches.at( i )->vertexNumberFromVertexId( QgsVertexId( 0, id.ring, id.vertex ) ); + if ( partNumber == -1 ) + { + return -1; + } + + return number + partNumber; + } + else + { + number += mPatches.at( i )->nCoordinates(); + } + } + + return -1; // should not happen +} + +bool QgsPolyhedralSurface::isEmpty() const +{ + return mPatches.isEmpty(); +} + +double QgsPolyhedralSurface::closestSegment( const QgsPoint &pt, QgsPoint &segmentPt, QgsVertexId &vertexAfter, int *leftOf, double epsilon ) const +{ + QVector segmentList = mPatches; + return QgsGeometryUtils::closestSegmentFromComponents( segmentList, QgsGeometryUtils::Part, pt, segmentPt, vertexAfter, leftOf, epsilon ); +} + +bool QgsPolyhedralSurface::nextVertex( QgsVertexId &vId, QgsPoint &vertex ) const +{ + if ( vId.part < 0 ) + { + vId.part = 0; + vId.ring = -1; + vId.vertex = -1; + } + + if ( isEmpty() || vId.part >= partCount() ) + { + return false; + } + + QgsPolygon *patch = mPatches[vId.part]; + if ( patch->nextVertex( vId, vertex ) ) + { + return true; + } + + ++vId.part; + vId.ring = 0; + vId.vertex = -1; + if ( vId.part >= partCount() ) + { + return false; + } + patch = mPatches[vId.part]; + return patch->nextVertex( vId, vertex ); +} + +void QgsPolyhedralSurface::adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex, QgsVertexId &nextVertex ) const +{ + if ( vertex.part < 0 || vertex.part >= partCount() ) + { + previousVertex = QgsVertexId(); + nextVertex = QgsVertexId(); + return; + } + + QgsPolygon *patch = mPatches[vertex.ring]; + patch->adjacentVertices( QgsVertexId( 0, 0, vertex.vertex ), previousVertex, nextVertex ); + return; +} + +bool QgsPolyhedralSurface::insertVertex( QgsVertexId vId, const QgsPoint &vertex ) +{ + if ( vId.part < 0 || vId.part >= partCount() ) + { + return false; + } + + QgsPolygon *patch = mPatches.at( vId.part ); + bool success = patch->insertVertex( QgsVertexId( 0, vId.ring, vId.vertex ), vertex ); + if ( success ) + { + clearCache(); + } + + return success; +} + +bool QgsPolyhedralSurface::moveVertex( QgsVertexId vId, const QgsPoint &newPos ) +{ + if ( vId.part < 0 || vId.part >= partCount() ) + { + return false; + } + + QgsPolygon *patch = mPatches.at( vId.part ); + bool success = patch->moveVertex( QgsVertexId( 0, vId.ring, vId.vertex ), newPos ); + if ( success ) + { + clearCache(); + } + + return success; +} + +bool QgsPolyhedralSurface::deleteVertex( QgsVertexId vId ) +{ + if ( vId.part < 0 || vId.part >= partCount() ) + { + return false; + } + + QgsPolygon *patch = mPatches.at( vId.part ); + bool success = patch->deleteVertex( QgsVertexId( 0, vId.ring, vId.vertex ) ); + if ( success ) + { + // if the patch has lost its exterior ring, remove it + if ( !patch->exteriorRing() ) + { + delete mPatches.takeAt( vId.part ); + } + clearCache(); + } + + return success; +} + +bool QgsPolyhedralSurface::hasCurvedSegments() const +{ + return false; +} + +QgsAbstractGeometry *QgsPolyhedralSurface::segmentize( double tolerance, SegmentationToleranceType toleranceType ) const +{ + // This is only used by curves + Q_UNUSED( tolerance ) + Q_UNUSED( toleranceType ) + return clone(); +} + +double QgsPolyhedralSurface::vertexAngle( QgsVertexId vertex ) const +{ + if ( vertex.part < 0 || vertex.part >= partCount() ) + { + return 0.0; + } + + QgsPolygon *patch = mPatches[vertex.part]; + return patch->vertexAngle( QgsVertexId( 0, vertex.ring, vertex.vertex ) ); +} + +int QgsPolyhedralSurface::vertexCount( int part, int ring ) const +{ + if ( part < 0 || part >= partCount() ) + { + return 0; + } + + QgsPolygon *patchPolygon = mPatches[part]; + QgsCurve *ringCurve = ring == 0 ? patchPolygon->exteriorRing() : patchPolygon->interiorRing( ring - 1 ); + if ( ringCurve ) + { + return ringCurve->vertexCount(); + } + + return 0; +} + +int QgsPolyhedralSurface::ringCount( int part ) const +{ + if ( part < 0 || part >= partCount() ) + return 0; + + return mPatches[part]->ringCount(); +} + +int QgsPolyhedralSurface::partCount() const +{ + return mPatches.size(); +} + +QgsPoint QgsPolyhedralSurface::vertexAt( QgsVertexId id ) const +{ + if ( id.part < 0 || id.part >= partCount() ) + return QgsPoint(); + + return mPatches[id.part]->vertexAt( id ); +} + +double QgsPolyhedralSurface::segmentLength( QgsVertexId startVertex ) const +{ + if ( startVertex.part < 0 || startVertex.part >= partCount() ) + { + return 0.0; + } + + const QgsPolygon *patch = mPatches[startVertex.part]; + return patch->segmentLength( QgsVertexId( 0, startVertex.ring, startVertex.vertex ) ); +} + +bool QgsPolyhedralSurface::addZValue( double zValue ) +{ + if ( QgsWkbTypes::hasZ( mWkbType ) ) + { + return false; + } + + mWkbType = QgsWkbTypes::addZ( mWkbType ); + + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->addZValue( zValue ); + } + clearCache(); + return true; +} + +bool QgsPolyhedralSurface::addMValue( double mValue ) +{ + if ( QgsWkbTypes::hasM( mWkbType ) ) + { + return false; + } + + mWkbType = QgsWkbTypes::addM( mWkbType ); + + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->addMValue( mValue ); + } + clearCache(); + return true; +} + +bool QgsPolyhedralSurface::dropZValue() +{ + if ( !is3D() ) + { + return false; + } + + mWkbType = QgsWkbTypes::dropZ( mWkbType ); + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->dropZValue(); + } + clearCache(); + return true; +} + +bool QgsPolyhedralSurface::dropMValue() +{ + if ( !isMeasure() ) + { + return false; + } + + mWkbType = QgsWkbTypes::dropM( mWkbType ); + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->dropMValue(); + } + clearCache(); + return true; +} + +void QgsPolyhedralSurface::swapXy() +{ + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->swapXy(); + } + clearCache(); +} + +QgsMultiSurface *QgsPolyhedralSurface::toCurveType() const +{ + std::unique_ptr multiSurface = std::make_unique< QgsMultiSurface >(); + multiSurface->reserve( mPatches.size() ); + for ( const QgsPolygon *polygon : std::as_const( mPatches ) ) + { + multiSurface->addGeometry( polygon->clone() ); + } + return multiSurface.release(); +} + +bool QgsPolyhedralSurface::transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback ) +{ + if ( !transformer ) + return false; + + bool res = true; + + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + res = patch->transform( transformer ); + if ( feedback && feedback->isCanceled() ) + res = false; + + if ( !res ) + break; + } + + clearCache(); + return res; +} + +QgsMultiPolygon *QgsPolyhedralSurface::toMultiPolygon() const +{ + std::unique_ptr multiPolygon = std::make_unique< QgsMultiPolygon >(); + multiPolygon->reserve( mPatches.size() ); + for ( const QgsPolygon *polygon : std::as_const( mPatches ) ) + { + multiPolygon->addGeometry( polygon->clone() ); + } + return multiPolygon.release(); +} + +void QgsPolyhedralSurface::filterVertices( const std::function &filter ) +{ + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->filterVertices( filter ); + } + + clearCache(); +} + +void QgsPolyhedralSurface::transformVertices( const std::function &transform ) +{ + for ( QgsPolygon *patch : std::as_const( mPatches ) ) + { + patch->transformVertices( transform ); + } + + clearCache(); +} + +int QgsPolyhedralSurface::childCount() const +{ + return mPatches.count(); +} + +QgsAbstractGeometry *QgsPolyhedralSurface::childGeometry( int index ) const +{ + return mPatches.at( index ); +} + +int QgsPolyhedralSurface::compareToSameClass( const QgsAbstractGeometry *other ) const +{ + const QgsPolyhedralSurface *otherPolySurface = qgsgeometry_cast( other ); + if ( !otherPolySurface ) + return -1; + + const int nPatches1 = mPatches.size(); + const int nPatches2 = otherPolySurface->mPatches.size(); + if ( nPatches1 < nPatches2 ) + { + return -1; + } + if ( nPatches1 > nPatches2 ) + { + return 1; + } + + for ( int i = 0; i < nPatches1; i++ ) + { + const int polygonComp = mPatches.at( i )->compareTo( otherPolySurface->mPatches.at( i ) ); + if ( polygonComp != 0 ) + { + return polygonComp; + } + } + + return 0; +} diff --git a/src/core/geometry/qgspolyhedralsurface.h b/src/core/geometry/qgspolyhedralsurface.h new file mode 100644 index 00000000000..d1ac553e122 --- /dev/null +++ b/src/core/geometry/qgspolyhedralsurface.h @@ -0,0 +1,376 @@ +/*************************************************************************** + qgspolyhedralsurface.h + ------------------- + begin : August 2024 + copyright : (C) 2024 by Jean Felder + email : jean dot felder at oslandia dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSPOLYHEDRALSURFACE_H +#define QGSPOLYHEDRALSURFACE_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgssurface.h" +#include "qgspolygon.h" +#include "qgsmultipolygon.h" + + +/** + * \ingroup core + * \class QgsPolyhedralSurface + * \brief Polyhedral surface geometry type. + * + * A polyhedral surface is a collection of polygons which share common boundary segments. + * + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsPolyhedralSurface: public QgsSurface +{ + public: + QgsPolyhedralSurface(); + QgsPolyhedralSurface( const QgsPolyhedralSurface &p ); + + + /** + * Creates a polyhedral surface from a multiPolygon. + */ + QgsPolyhedralSurface( const QgsMultiPolygon *multiPolygon ); + + QgsPolyhedralSurface &operator=( const QgsPolyhedralSurface &p ); + +#ifndef SIP_RUN + private: + bool fuzzyHelper( const QgsAbstractGeometry &other, double epsilon, bool useDistance ) const + { + const QgsPolyhedralSurface *otherPolygon = qgsgeometry_cast< const QgsPolyhedralSurface * >( &other ); + if ( !otherPolygon ) + return false; + + //run cheap checks first + if ( mWkbType != otherPolygon->mWkbType ) + return false; + + if ( mPatches.count() != otherPolygon->mPatches.count() ) + return false; + + for ( int i = 0; i < mPatches.count(); ++i ) + { + if ( ( !mPatches.at( i ) && otherPolygon->mPatches.at( i ) ) || + ( mPatches.at( i ) && !otherPolygon->mPatches.at( i ) ) ) + return false; + + if ( useDistance ) + { + if ( mPatches.at( i ) && otherPolygon->mPatches.at( i ) && + !( *mPatches.at( i ) ).fuzzyDistanceEqual( *otherPolygon->mPatches.at( i ), epsilon ) ) + return false; + } + else + { + if ( mPatches.at( i ) && otherPolygon->mPatches.at( i ) && + !( *mPatches.at( i ) ).fuzzyEqual( *otherPolygon->mPatches.at( i ), epsilon ) ) + return false; + } + } + + return true; + } +#endif + + public: + bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const override SIP_HOLDGIL + { + return fuzzyHelper( other, epsilon, false ); + } + bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const override SIP_HOLDGIL + { + return fuzzyHelper( other, epsilon, true ); + } + + bool operator==( const QgsAbstractGeometry &other ) const override + { + return fuzzyEqual( other, 1e-8 ); + } + + bool operator!=( const QgsAbstractGeometry &other ) const override + { + return !operator==( other ); + } + + ~QgsPolyhedralSurface() override; + + QString geometryType() const override SIP_HOLDGIL; + int dimension() const final SIP_HOLDGIL; + QgsPolyhedralSurface *clone() const override SIP_FACTORY; + void clear() override; + + bool fromWkb( QgsConstWkbPtr &wkb ) override; + bool fromWkt( const QString &wkt ) override; + + int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const override; + QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const override; + QString asWkt( int precision = 17 ) const override; + QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override; + QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override; + json asJsonObject( int precision = 17 ) const override SIP_SKIP; + QString asKml( int precision = 17 ) const override; + void normalize() override SIP_HOLDGIL; + + //surface interface + double area() const override SIP_HOLDGIL; + double perimeter() const override SIP_HOLDGIL; + QgsAbstractGeometry *boundary() const override SIP_FACTORY; + QgsPolyhedralSurface *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const override SIP_FACTORY; + QgsPolyhedralSurface *simplifyByDistance( double tolerance ) const override SIP_FACTORY; + bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits::epsilon(), bool useZValues = false ) override; + bool boundingBoxIntersects( const QgsBox3D &box3d ) const override SIP_HOLDGIL; + + /** + * Returns the number of patches contained with the polyhedral surface. + * + * \see patchN() + */ + int numPatches() const SIP_HOLDGIL + { + return mPatches.size(); + } + +#ifndef SIP_RUN + + /** + * Retrieves a patch from the polyhedral surface. The first patch has index 0. + * + * \see numPatches() + */ + const QgsPolygon *patchN( int i ) const + { + if ( i < 0 || i >= mPatches.size() ) + { + return nullptr; + } + return mPatches.at( i ); + } + + /** + * Retrieves a patch from the polyhedral surface. The first patch has index 0. + * + * \see numPatches() + */ + QgsPolygon *patchN( int i ) + { + if ( i < 0 || i >= mPatches.size() ) + { + return nullptr; + } + return mPatches.at( i ); + } +#else + + /** + * Retrieves a patch from the polyhedral surface. The first patch has index 0. + * + * \throws IndexError if no patch with the specified index exists. + * + * \see numPatches() + */ + SIP_PYOBJECT patchN( int i ) SIP_HOLDGIL SIP_TYPEHINT( QgsPolygon ); + % MethodCode + if ( a0 < 0 || a0 >= sipCpp->numPatches() ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + return sipConvertFromType( const_cast< QgsPolygon * >( sipCpp->patchN( a0 ) ), sipType_QgsPolygon, NULL ); + } + % End +#endif + + /** + * Sets all patches, transferring ownership to the polyhedral surface. + */ + virtual void setPatches( const QVector &patches SIP_TRANSFER ); + + /** + * Adds a patch to the geometry, transferring ownership to the polyhedral surface. + */ + virtual void addPatch( QgsPolygon *patch SIP_TRANSFER ); + +#ifndef SIP_RUN + + /** + * Removes a patch from the polyhedral surface. The first patch has index 0. + * The corresponding patch is removed from the polyhedral surface and deleted. If a patch was successfully removed + * the function will return TRUE. + * + */ + bool removePatch( int patchIndex ); +#else + + /** + * Removes a patch from the polyhedral surface. The first patch has index 0. + * The corresponding patch is removed from the polyhedral surface and deleted. + * + * \throws IndexError if no patch with the specified index exists. + * + */ + bool removePatch( int ringIndex ); + % MethodCode + if ( a0 < 0 || a0 >= sipCpp->numPatches() ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else + { + return PyBool_FromLong( sipCpp->removePatch( a0 ) ); + } + % End +#endif + + QPainterPath asQPainterPath() const override; + void draw( QPainter &p ) const override; + void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ) override SIP_THROW( QgsCsException ); + void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override; + + bool insertVertex( QgsVertexId position, const QgsPoint &vertex ) override; + bool moveVertex( QgsVertexId position, const QgsPoint &newPos ) override; + bool deleteVertex( QgsVertexId position ) override; + + QgsCoordinateSequence coordinateSequence() const override; + int nCoordinates() const override; + int vertexNumberFromVertexId( QgsVertexId id ) const override; + bool isEmpty() const override SIP_HOLDGIL; + double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt SIP_OUT, QgsVertexId &vertexAfter SIP_OUT, int *leftOf SIP_OUT = nullptr, double epsilon = 4 * std::numeric_limits::epsilon() ) const override; + + bool nextVertex( QgsVertexId &id, QgsPoint &vertex SIP_OUT ) const override; + void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex SIP_OUT, QgsVertexId &nextVertex SIP_OUT ) const override; + bool hasCurvedSegments() const final; + + /** + * Returns a geometry without curves. Caller takes ownership + * \param tolerance segmentation tolerance + * \param toleranceType maximum segmentation angle or maximum difference between approximation and curve + */ + QgsAbstractGeometry *segmentize( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const override SIP_FACTORY; + + /** + * Returns approximate rotation angle for a vertex. Usually average angle between adjacent segments. + * \param vertex the vertex id + * \returns rotation in radians, clockwise from north + */ + double vertexAngle( QgsVertexId vertex ) const override; + + int vertexCount( int part = 0, int ring = 0 ) const override; + int ringCount( int part = 0 ) const override SIP_HOLDGIL; + int partCount() const override SIP_HOLDGIL; + QgsPoint vertexAt( QgsVertexId id ) const override; + double segmentLength( QgsVertexId startVertex ) const override; + + bool addZValue( double zValue = 0 ) override; + bool addMValue( double mValue = 0 ) override; + bool dropZValue() override; + bool dropMValue() override; + void swapXy() override; + + QgsMultiSurface *toCurveType() const override SIP_FACTORY; + + bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = nullptr ) override; + + /** + * Converts a polyhedral surface to a multipolygon. + * Caller takes ownership. + */ + QgsMultiPolygon *toMultiPolygon() const SIP_FACTORY; + +#ifndef SIP_RUN + void filterVertices( const std::function< bool( const QgsPoint & ) > &filter ) override; + void transformVertices( const std::function< QgsPoint( const QgsPoint & ) > &transform ) override; + + /** + * Cast the \a geom to a QgsPolyhedralSurface. + * Should be used by qgsgeometry_cast( geometry ). + * + * \note Not available in Python. Objects will be automatically be converted to the appropriate target type. + */ + inline static const QgsPolyhedralSurface *cast( const QgsAbstractGeometry *geom ) + { + if ( geom && QgsWkbTypes::flatType( geom->wkbType() ) == Qgis::WkbType::PolyhedralSurface ) + return static_cast( geom ); + + return nullptr; + } +#endif + + QgsPolyhedralSurface *createEmptyWithSameType() const override SIP_FACTORY; + +#ifdef SIP_RUN + SIP_PYOBJECT __repr__(); + % MethodCode + QString wkt = sipCpp->asWkt(); + if ( wkt.length() > 1000 ) + wkt = wkt.left( 1000 ) + QStringLiteral( "..." ); + QString str = QStringLiteral( "" ).arg( wkt ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); + % End + + /** + * Returns the number of patches within the polyhedral surface. + */ + int __len__() const; + % MethodCode + sipRes = sipCpp->numPatches(); + % End + + /** + * Returns the geometry at the specified ``index``. + * + * Indexes can be less than 0, in which case they correspond to geometries from the end of the collect. E.g. an index of -1 + * corresponds to the last geometry in the collection. + * + * \throws IndexError if no geometry with the specified ``index`` exists. + */ + SIP_PYOBJECT __getitem__( int index ) SIP_TYPEHINT( QgsPolygon ); + % MethodCode + const int count = sipCpp->numPatches(); + if ( a0 < -count || a0 >= count ) + { + PyErr_SetString( PyExc_IndexError, QByteArray::number( a0 ) ); + sipIsErr = 1; + } + else if ( a0 >= 0 ) + { + return sipConvertFromType( sipCpp->patchN( a0 ), sipType_QgsPolygon, NULL ); + } + else + { + return sipConvertFromType( sipCpp->patchN( count + a0 ), sipType_QgsPolygon, NULL ); + } + % End +#endif + + protected: + + int childCount() const override; + QgsAbstractGeometry *childGeometry( int index ) const override; + int compareToSameClass( const QgsAbstractGeometry *other ) const override; + QgsBox3D calculateBoundingBox3D() const override; + + private: + QVector< QgsPolygon * > mPatches; +}; + +// clazy:excludeall=qstring-allocations + +#endif // QGSPOLYHEDRALSURFACE_H diff --git a/tests/src/core/geometry/CMakeLists.txt b/tests/src/core/geometry/CMakeLists.txt index ce16cf24619..3e25129d1ba 100644 --- a/tests/src/core/geometry/CMakeLists.txt +++ b/tests/src/core/geometry/CMakeLists.txt @@ -28,6 +28,7 @@ set(TESTS testqgspoint.cpp testqgspointxy.cpp testqgspolygon.cpp + testqgspolyhedralsurface.cpp testqgsquadrilateral.cpp testqgsrectangle.cpp testqgsregularpolygon.cpp diff --git a/tests/src/core/geometry/testqgspolyhedralsurface.cpp b/tests/src/core/geometry/testqgspolyhedralsurface.cpp new file mode 100644 index 00000000000..aa8ec6cf68b --- /dev/null +++ b/tests/src/core/geometry/testqgspolyhedralsurface.cpp @@ -0,0 +1,1696 @@ +/*************************************************************************** + testqgspolyhedralsurface.cpp + -------------------------------------- + Date : August 2024 + Copyright : (C) 2024 by Jean Felder + Email : jean dot felder at oslandia dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include "qgstest.h" +#include +#include +#include + +#include "qgslinestring.h" +#include "qgsmultilinestring.h" +#include "qgsmultipolygon.h" +#include "qgspolygon.h" +#include "qgssurface.h" +#include "qgspolyhedralsurface.h" +#include "qgsvertexid.h" +#include "testgeometryutils.h" + +class TestQgsPolyhedralSurface: public QObject +{ + Q_OBJECT + + private slots: + void testConstructor(); + void testCopyConstructor(); + void testClear(); + void testClone(); + void testEquality(); + void testAddPatch(); + void testRemovePatch(); + void test3DPatches(); + void testAreaPerimeter(); + void testInsertVertex(); + void testMoveVertex(); + void testDeleteVertex(); + void testNextVertex(); + void testVertexAngle(); + void testDeleteVertexRemovePatch(); + void testVertexNumberFromVertexId(); + void testHasCurvedSegments(); + void testClosestSegment(); + void testBoundary(); + void testBoundingBox(); + void testBoundingBox3D(); + void testBoundingBoxIntersects(); + void testDropZValue(); + void testDropMValue(); + void testCoordinateSequence(); + void testChildGeometry(); + void testWKB(); + void testWKT(); + void testExport(); + void testCast(); +}; + +void TestQgsPolyhedralSurface::testConstructor() +{ + QgsPolyhedralSurface polySurfaceEmpty; + + QVERIFY( polySurfaceEmpty.isEmpty() ); + QCOMPARE( polySurfaceEmpty.nCoordinates(), 0 ); + QCOMPARE( polySurfaceEmpty.ringCount(), 0 ); + QCOMPARE( polySurfaceEmpty.numPatches(), 0 ); + QCOMPARE( polySurfaceEmpty.partCount(), 0 ); + QVERIFY( !polySurfaceEmpty.is3D() ); + QVERIFY( !polySurfaceEmpty.isMeasure() ); + QCOMPARE( polySurfaceEmpty.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( polySurfaceEmpty.wktTypeStr(), QString( "PolyhedralSurface" ) ); + QCOMPARE( polySurfaceEmpty.geometryType(), QString( "PolyhedralSurface" ) ); + QCOMPARE( polySurfaceEmpty.dimension(), 2 ); + QVERIFY( !polySurfaceEmpty.hasCurvedSegments() ); + QCOMPARE( polySurfaceEmpty.area(), 0.0 ); + QCOMPARE( polySurfaceEmpty.perimeter(), 0.0 ); + QVERIFY( !polySurfaceEmpty.patchN( 0 ) ); + + + std::unique_ptr multiPolygon = std::make_unique(); + QgsPolygon part; + QgsLineString ring; + ring.setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 5, 50, 1, 4 ) + << QgsPoint( Qgis::WkbType::PointZM, 6, 61, 3, 5 ) + << QgsPoint( Qgis::WkbType::PointZM, 9, 71, 4, 15 ) + << QgsPoint( Qgis::WkbType::PointZM, 5, 71, 4, 6 ) ) ; + part.setExteriorRing( ring.clone() ); + multiPolygon->addGeometry( part.clone() ); + QgsPolyhedralSurface polySurface( multiPolygon.get() ); + QVERIFY( multiPolygon ); + QVERIFY( !polySurface.isEmpty() ); + QCOMPARE( polySurface.nCoordinates(), 5 ); + QCOMPARE( polySurface.ringCount(), 1 ); + QCOMPARE( polySurface.numPatches(), 1 ); + QCOMPARE( polySurface.partCount(), 1 ); + QVERIFY( polySurface.is3D() ); + QVERIFY( polySurface.isMeasure() ); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceZM ); + QCOMPARE( polySurface.wktTypeStr(), QString( "PolyhedralSurfaceZM" ) ); + QCOMPARE( polySurface.geometryType(), QString( "PolyhedralSurface" ) ); + QCOMPARE( polySurface.dimension(), 2 ); + QVERIFY( !polySurface.hasCurvedSegments() ); + QGSCOMPARENEAR( polySurface.area(), 30.5, 0.01 ); + QGSCOMPARENEAR( polySurface.perimeter(), 46.49, 0.01 ); + QVERIFY( polySurface.patchN( 0 ) ); + QCOMPARE( polySurface.numPatches(), multiPolygon->numGeometries() ); +} + +void TestQgsPolyhedralSurface::testCopyConstructor() +{ + QgsPolyhedralSurface polySurface1; + QCOMPARE( polySurface1.numPatches(), 0 ); + + QgsPolyhedralSurface polySurface2( polySurface1 ); + QCOMPARE( polySurface1, polySurface2 ); + + // add a patch to polySurface1 + QgsPolygon patch; + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) + << QgsPoint( 0, 10 ) << QgsPoint( 10, 10 ) + << QgsPoint( 10, 0 ) << QgsPoint( 0, 0 ) ); + patch.setExteriorRing( patchExterior ); + QgsLineString *patchInteriorRing1 = new QgsLineString(); + patchInteriorRing1->setPoints( QgsPointSequence() << QgsPoint( 1, 1 ) + << QgsPoint( 1, 9 ) << QgsPoint( 9, 9 ) + << QgsPoint( 9, 1 ) << QgsPoint( 1, 1 ) ); + patch.addInteriorRing( patchInteriorRing1 ); + polySurface1.addPatch( patch.clone() ); + QCOMPARE( polySurface1.numPatches(), 1 ); + + QgsPolyhedralSurface polySurface3( polySurface1 ); + QCOMPARE( polySurface1, polySurface3 ); + + QgsPolyhedralSurface polySurface4; + polySurface4 = polySurface2; + QCOMPARE( polySurface2, polySurface4 ); + polySurface4 = polySurface1; + QCOMPARE( polySurface1, polySurface4 ); +} + +void TestQgsPolyhedralSurface::testClear() +{ + QgsPolyhedralSurface polySurface; + + QgsPolygon *patch = new QgsPolygon; + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 0, 0, 1 ) + << QgsPoint( 0, 10, 1 ) << QgsPoint( 10, 10, 1 ) + << QgsPoint( 10, 0, 1 ) << QgsPoint( 0, 0, 1 ) ); + patch->setExteriorRing( patchExterior ); + QgsLineString *patchInteriorRing1 = new QgsLineString(); + patchInteriorRing1->setPoints( QgsPointSequence() << QgsPoint( 1, 1, 1 ) + << QgsPoint( 1, 9, 1 ) << QgsPoint( 9, 9, 1 ) + << QgsPoint( 9, 1, 1 ) << QgsPoint( 1, 1, 1 ) ); + patch->addInteriorRing( patchInteriorRing1 ); + polySurface.addPatch( patch ); + + QVERIFY( !polySurface.isEmpty() ); + QCOMPARE( polySurface.numPatches(), 1 ); + QCOMPARE( polySurface.nCoordinates(), 10 ); + QCOMPARE( polySurface.numPatches(), 1 ); + QCOMPARE( polySurface.partCount(), 1 ); + QVERIFY( polySurface.is3D() ); + QVERIFY( !polySurface.isMeasure() ); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceZ ); + + polySurface.clear(); + QVERIFY( polySurface.isEmpty() ); + QCOMPARE( polySurface.numPatches(), 0 ); + QCOMPARE( polySurface.nCoordinates(), 0 ); + QCOMPARE( polySurface.numPatches(), 0 ); + QCOMPARE( polySurface.partCount(), 0 ); + QVERIFY( !polySurface.is3D() ); + QVERIFY( !polySurface.isMeasure() ); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); +} + +void TestQgsPolyhedralSurface::testClone() +{ + QgsPolyhedralSurface polySurface; + + std::unique_ptr< QgsPolyhedralSurface >cloned( polySurface.clone() ); + QCOMPARE( polySurface, *cloned ); + + QgsPolygon *patch = new QgsPolygon(); + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 4, 4 ) + << QgsPoint( 6, 10 ) << QgsPoint( 10, 10 ) + << QgsPoint( 10, 4 ) << QgsPoint( 4, 4 ) ); + patch->setExteriorRing( patchExterior ); + QgsLineString *patchInteriorRing1 = new QgsLineString(); + patchInteriorRing1->setPoints( QgsPointSequence() << QgsPoint( 7, 5 ) + << QgsPoint( 8, 9 ) << QgsPoint( 9, 9 ) + << QgsPoint( 9, 6 ) << QgsPoint( 7, 5 ) ); + patch->addInteriorRing( patchInteriorRing1 ); + polySurface.addPatch( patch ); + + cloned.reset( polySurface.clone() ); + QCOMPARE( polySurface, *cloned ); +} + +void TestQgsPolyhedralSurface::testEquality() +{ + QgsPolyhedralSurface polySurface1, polySurface2; + QgsPolygon *patch; + + QVERIFY( polySurface1 == polySurface2 ); + QVERIFY( !( polySurface1 != polySurface2 ) ); + + patch = new QgsPolygon(); + QgsLineString patchExterior1( QVector() << QgsPoint( 0, 0 ) + << QgsPoint( 0, 10 ) << QgsPoint( 10, 10 ) + << QgsPoint( 10, 0 ) << QgsPoint( 0, 0 ) ); + QgsLineString patchInteriorRing1( QVector< QgsPoint >() << QgsPoint( 1, 1 ) + << QgsPoint( 2, 1 ) << QgsPoint( 2, 2 ) + << QgsPoint( 1, 2 ) << QgsPoint( 1, 1 ) ); + patch->setExteriorRing( patchExterior1.clone() ); + patch->addInteriorRing( patchInteriorRing1.clone() ); + polySurface1.addPatch( patch ); + QVERIFY( !( polySurface1 == polySurface2 ) ); + QVERIFY( polySurface1 != polySurface2 ); + + patch = new QgsPolygon(); + patch->setExteriorRing( patchExterior1.clone() ); + patch->addInteriorRing( patchInteriorRing1.clone() ); + polySurface2.addPatch( patch ); + QVERIFY( polySurface1 == polySurface2 ); + QVERIFY( !( polySurface1 != polySurface2 ) ); + + patch = new QgsPolygon(); + QgsLineString patchExterior2( QVector() << QgsPoint( 10, 0 ) + << QgsPoint( 10, 10 ) << QgsPoint( 15, 10 ) + << QgsPoint( 15, 0 ) << QgsPoint( 10, 0 ) ); + QgsLineString patchInteriorRing2( QVector< QgsPoint >() << QgsPoint( 13, 1 ) + << QgsPoint( 14, 1 ) << QgsPoint( 14, 2 ) + << QgsPoint( 13, 2 ) << QgsPoint( 13, 1 ) ); + patch->setExteriorRing( patchExterior2.clone() ); + patch->addInteriorRing( patchInteriorRing2.clone() ); + polySurface2.addPatch( patch ); + QVERIFY( !( polySurface1 == polySurface2 ) ); + QVERIFY( polySurface1 != polySurface2 ); + + polySurface2.removePatch( 1 ); + QVERIFY( polySurface1 == polySurface2 ); + QVERIFY( !( polySurface1 != polySurface2 ) ); +} + +void TestQgsPolyhedralSurface::testAddPatch() +{ + QgsPolyhedralSurface polySurface; + + // empty surface + QCOMPARE( polySurface.numPatches(), 0 ); + QVERIFY( !polySurface.patchN( -1 ) ); + QVERIFY( !polySurface.patchN( 0 ) ); + + polySurface.addPatch( nullptr ); + QCOMPARE( polySurface.numPatches(), 0 ); + + QgsPolygon *patch = new QgsPolygon(); + QgsLineString patchExterior; + patchExterior.setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) + << QgsPoint( 0, 10 ) << QgsPoint( 10, 10 ) + << QgsPoint( 10, 0 ) << QgsPoint( 0, 0 ) ); + patch->setExteriorRing( patchExterior.clone() ); + QgsLineString patchInteriorRing; + patchInteriorRing.setPoints( QgsPointSequence() << QgsPoint( 1, 1 ) + << QgsPoint( 1, 9 ) << QgsPoint( 9, 9 ) + << QgsPoint( 9, 1 ) << QgsPoint( 1, 1 ) ); + patch->addInteriorRing( patchInteriorRing.clone() ); + polySurface.addPatch( patch ); + + QCOMPARE( polySurface.numPatches(), 1 ); + QCOMPARE( polySurface.patchN( 0 ), patch ); + QVERIFY( !polySurface.patchN( 1 ) ); + + QCOMPARE( polySurface.nCoordinates(), 10 ); + + // try adding a patch with z to a 2d polyhedral surface, z should be dropped + patch = new QgsPolygon(); + QgsLineString patchExteriorZ; + patchExteriorZ.setPoints( QgsPointSequence() << QgsPoint( 10, 0, 1 ) + << QgsPoint( 0, 10, 1 ) << QgsPoint( 10, 10, 1 ) + << QgsPoint( 10, 0, 1 ) << QgsPoint( 0, 0, 1 ) ); + patch->setExteriorRing( patchExteriorZ.clone() ); + QgsLineString patchInteriorRingZ; + patchInteriorRingZ.setPoints( QgsPointSequence() << QgsPoint( 1, 1, 1 ) + << QgsPoint( 1, 9, 1 ) << QgsPoint( 9, 9, 1 ) + << QgsPoint( 9, 1, 1 ) << QgsPoint( 1, 1, 1 ) ); + patch->addInteriorRing( patchInteriorRingZ.clone() ); + polySurface.addPatch( patch ); + + QCOMPARE( polySurface.numPatches(), 2 ); + QVERIFY( !polySurface.is3D() ); + QVERIFY( !polySurface.isMeasure() ); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QVERIFY( polySurface.patchN( 1 ) ); + QVERIFY( !polySurface.patchN( 1 )->is3D() ); + QVERIFY( !polySurface.patchN( 1 )->isMeasure() ); + QCOMPARE( polySurface.patchN( 1 )->wkbType(), Qgis::WkbType::Polygon ); + + // try adding a patch with zm to a 2d polygon, z and m should be dropped + patch = new QgsPolygon; + QgsLineString patchExteriorZM; + patchExteriorZM.setPoints( QgsPointSequence() << QgsPoint( 10, 0, 1, 2 ) + << QgsPoint( 0, 10, 1, 2 ) << QgsPoint( 10, 10, 1, 2 ) + << QgsPoint( 10, 0, 1, 2 ) << QgsPoint( 0, 0, 1, 2 ) ); + patch->setExteriorRing( patchExteriorZM.clone() ); + QgsLineString patchInteriorRingZM; + patchInteriorRingZM.setPoints( QgsPointSequence() << QgsPoint( 1, 1, 1, 2 ) + << QgsPoint( 1, 9, 1, 2 ) << QgsPoint( 9, 9, 1, 2 ) + << QgsPoint( 9, 1, 1, 2 ) << QgsPoint( 1, 1, 1, 2 ) ); + patch->addInteriorRing( patchInteriorRingZM.clone() ); + polySurface.addPatch( patch ); + + QCOMPARE( polySurface.numPatches(), 3 ); + QVERIFY( !polySurface.is3D() ); + QVERIFY( !polySurface.isMeasure() ); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QVERIFY( polySurface.patchN( 2 ) ); + QVERIFY( !polySurface.patchN( 2 )->is3D() ); + QVERIFY( !polySurface.patchN( 2 )->isMeasure() ); + QCOMPARE( polySurface.patchN( 2 )->wkbType(), Qgis::WkbType::Polygon ); + + + // addPatch without z/m to PolygonZM + QgsPolyhedralSurface polySurface2; + patch = new QgsPolygon(); + patch->setExteriorRing( patchExteriorZM.clone() ); + patch->addInteriorRing( patchInteriorRingZM.clone() ); + polySurface2.addPatch( patch ); + + QCOMPARE( polySurface2.numPatches(), 1 ); + QVERIFY( polySurface2.is3D() ); + QVERIFY( polySurface2.isMeasure() ); + QCOMPARE( polySurface2.wkbType(), Qgis::WkbType::PolyhedralSurfaceZM ); + QVERIFY( polySurface2.patchN( 0 ) ); + QVERIFY( polySurface2.patchN( 0 )->is3D() ); + QVERIFY( polySurface2.patchN( 0 )->isMeasure() ); + QCOMPARE( polySurface2.patchN( 0 )->wkbType(), Qgis::WkbType::PolygonZM ); + QCOMPARE( polySurface2.patchN( 0 )->vertexAt( QgsVertexId( 0, 0, 0 ) ), QgsPoint( Qgis::WkbType::PointZM, 10, 0, 1, 2 ) ); + + // patch has no z + patch = new QgsPolygon(); + patch->setExteriorRing( patchExterior.clone() ); + patch->addInteriorRing( patchInteriorRing.clone() ); + polySurface2.addPatch( patch ); + + QCOMPARE( polySurface2.numPatches(), 2 ); + QVERIFY( polySurface2.patchN( 1 ) ); + QVERIFY( polySurface2.patchN( 1 )->is3D() ); + QVERIFY( polySurface2.patchN( 1 )->isMeasure() ); + QCOMPARE( polySurface2.patchN( 1 )->wkbType(), Qgis::WkbType::PolygonZM ); + QCOMPARE( polySurface2.patchN( 1 )->vertexAt( QgsVertexId( 0, 0, 0 ) ), QgsPoint( Qgis::WkbType::PointZM, 0, 0, 0, 0 ) ); + + // patch has no m + patch = new QgsPolygon(); + patch->setExteriorRing( patchExteriorZ.clone() ); + patch->addInteriorRing( patchInteriorRingZ.clone() ); + polySurface2.addPatch( patch ); + + QCOMPARE( polySurface2.numPatches(), 3 ); + QVERIFY( polySurface2.patchN( 2 ) ); + QVERIFY( polySurface2.patchN( 2 )->is3D() ); + QVERIFY( polySurface2.patchN( 2 )->isMeasure() ); + QCOMPARE( polySurface2.patchN( 2 )->wkbType(), Qgis::WkbType::PolygonZM ); + QCOMPARE( polySurface2.patchN( 2 )->vertexAt( QgsVertexId( 0, 0, 0 ) ), QgsPoint( Qgis::WkbType::PointZM, 10, 0, 1, 0 ) ); +} + +void TestQgsPolyhedralSurface::testRemovePatch() +{ + QgsPolyhedralSurface polySurface; + QVector< QgsPolygon * > patches; + + QVERIFY( !polySurface.removePatch( -1 ) ); + QVERIFY( !polySurface.removePatch( 0 ) ); + + QgsPolygon *patch1 = new QgsPolygon(); + QgsLineString *patchExterior1 = new QgsLineString(); + patchExterior1->setPoints( QgsPointSequence() << QgsPoint( 0.1, 0.1 ) + << QgsPoint( 0.1, 0.2 ) << QgsPoint( 0.2, 0.2 ) + << QgsPoint( 0.2, 0.1 ) << QgsPoint( 0.1, 0.1 ) ); + patch1->setExteriorRing( patchExterior1 ); + QgsLineString *patchInteriorRing1 = new QgsLineString(); + patchInteriorRing1->setPoints( QgsPointSequence() << QgsPoint( 0.12, 0.12 ) + << QgsPoint( 0.13, 0.13 ) << QgsPoint( 0.14, 0.14 ) + << QgsPoint( 0.14, 0.13 ) << QgsPoint( 0.12, 0.12 ) ); + patch1->addInteriorRing( patchInteriorRing1 ); + patches.append( patch1 ); + + QgsPolygon *patch2 = new QgsPolygon(); + QgsLineString *patchExterior2 = new QgsLineString(); + patchExterior2->setPoints( QgsPointSequence() << QgsPoint( 0.2, 0.1 ) + << QgsPoint( 0.2, 0.2 ) << QgsPoint( 0.3, 0.2 ) + << QgsPoint( 0.3, 0.1 ) << QgsPoint( 0.2, 0.1 ) ); + patch2->setExteriorRing( patchExterior2 ); + patches.append( patch2 ); + + QgsPolygon *patch3 = new QgsPolygon(); + QgsLineString *patchExterior3 = new QgsLineString(); + patchExterior3->setPoints( QgsPointSequence() << QgsPoint( 0.3, 0.1 ) + << QgsPoint( 0.3, 0.2 ) << QgsPoint( 0.4, 0.2 ) + << QgsPoint( 0.4, 0.1 ) << QgsPoint( 0.3, 0.1 ) ); + patch3->setExteriorRing( patchExterior3 ); + patches.append( patch3 ); + + polySurface.setPatches( patches ); + + QCOMPARE( polySurface.numPatches(), 3 ); + QCOMPARE( polySurface.patchN( 0 )->vertexAt( QgsVertexId( 0, 0, 0 ) ), QgsPoint( 0.1, 0.1 ) ); + + QVERIFY( polySurface.removePatch( 0 ) ); + QCOMPARE( polySurface.numPatches(), 2 ); + QCOMPARE( polySurface.patchN( 0 )->vertexAt( QgsVertexId( 0, 0, 0 ) ), QgsPoint( 0.2, 0.1 ) ); + QCOMPARE( polySurface.patchN( 1 )->vertexAt( QgsVertexId( 0, 0, 0 ) ), QgsPoint( 0.3, 0.1 ) ); + + QVERIFY( polySurface.removePatch( 1 ) ); + QCOMPARE( polySurface.numPatches(), 1 ); + QCOMPARE( polySurface.patchN( 0 )->vertexAt( QgsVertexId( 0, 0, 0 ) ), QgsPoint( 0.2, 0.1 ) ); + + QVERIFY( polySurface.removePatch( 0 ) ); + QCOMPARE( polySurface.numPatches(), 0 ); + QVERIFY( !polySurface.removePatch( 0 ) ); +} + +void TestQgsPolyhedralSurface::test3DPatches() +{ + // change dimensionality of patches using setExteriorRing + QgsPolyhedralSurface polySurface; + QVector< QgsPolygon * > patches; + + QgsPolygon *patch1 = new QgsPolygon(); + QgsLineString *patchExterior1 = new QgsLineString(); + patchExterior1->setPoints( QgsPointSequence() << QgsPoint( 0.1, 0.1, 2 ) + << QgsPoint( 0.1, 0.2, 2 ) << QgsPoint( 0.2, 0.2, 2 ) + << QgsPoint( 0.2, 0.1, 2 ) << QgsPoint( 0.1, 0.1, 2 ) ); + patch1->setExteriorRing( patchExterior1 ); + QgsLineString *patchInteriorRing1 = new QgsLineString(); + patchInteriorRing1->setPoints( QgsPointSequence() << QgsPoint( 0.12, 0.12, 2 ) + << QgsPoint( 0.13, 0.13, 2 ) << QgsPoint( 0.14, 0.14, 2 ) + << QgsPoint( 0.14, 0.13, 2 ) << QgsPoint( 0.12, 0.12, 2 ) ); + patch1->addInteriorRing( patchInteriorRing1 ); + patches.append( patch1 ); + + QgsPolygon *patch2 = new QgsPolygon(); + QgsLineString *patchExterior2 = new QgsLineString(); + patchExterior2->setPoints( QgsPointSequence() << QgsPoint( 0.2, 0.1, 2 ) + << QgsPoint( 0.2, 0.2, 2 ) << QgsPoint( 0.3, 0.2, 2 ) + << QgsPoint( 0.3, 0.1, 2 ) << QgsPoint( 0.2, 0.1, 2 ) ); + patch2->setExteriorRing( patchExterior2 ); + patches.append( patch2 ); + + polySurface.setPatches( patches ); + + QVERIFY( polySurface.is3D() ); + QVERIFY( !polySurface.isMeasure() ); + QVERIFY( polySurface.patchN( 0 )->is3D() ); + QVERIFY( !polySurface.patchN( 0 )->isMeasure() ); + QVERIFY( polySurface.patchN( 1 )->is3D() ); + QVERIFY( !polySurface.patchN( 1 )->isMeasure() ); +} + +void TestQgsPolyhedralSurface::testAreaPerimeter() +{ + QgsPolyhedralSurface polySurface; + + QgsPolygon *patch = new QgsPolygon(); + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 1 ) + << QgsPoint( 1, 6 ) << QgsPoint( 6, 6 ) + << QgsPoint( 6, 1 ) << QgsPoint( 1, 1 ) ); + patch->setExteriorRing( patchExterior ); + polySurface.addPatch( patch ); + + QGSCOMPARENEAR( polySurface.area(), 25.0, 0.01 ); // area is not implemented + QGSCOMPARENEAR( polySurface.perimeter(), 20.0, 0.01 ); +} + +void TestQgsPolyhedralSurface::testInsertVertex() +{ + QgsPolyhedralSurface polySurface; + QgsPolygon patch; + + // insert vertex in empty polygon + QVERIFY( !polySurface.insertVertex( QgsVertexId( 0, 0, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 0, 0, 1 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 0, 1, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 1, 0, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( polySurface.isEmpty() ); + + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) + << QgsPoint( 0.5, 0 ) << QgsPoint( 1, 0 ) + << QgsPoint( 2, 1 ) << QgsPoint( 1, 2 ) + << QgsPoint( 0, 2 ) << QgsPoint( 0, 0 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + QVERIFY( polySurface.insertVertex( QgsVertexId( 0, 0, 1 ), QgsPoint( 0.3, 0 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 8 ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 0, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 0.3, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 0.5, 0 ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 0, 0, -1 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 0, 0, 100 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 0, 1, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 1, 0, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + + // first vertex + QVERIFY( polySurface.insertVertex( QgsVertexId( 0, 0, 0 ), QgsPoint( 0, 0.1 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 9 ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 0, 0.1 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 0, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 0.3, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 0.5, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 7 ), QgsPoint( 0, 2 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 8 ), QgsPoint( 0, 0.1 ) ); + + // last vertex + QVERIFY( polySurface.insertVertex( QgsVertexId( 0, 0, 9 ), QgsPoint( 0.1, 0.1 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 10 ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 0.1, 0.1 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 0, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 0.3, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 0.5, 0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 8 ), QgsPoint( 0, 0.1 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 9 ), QgsPoint( 0.1, 0.1 ) ); + + // add a second patch with an interior ring + patch.clear(); + patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 10, 10 ) + << QgsPoint( 12, 10 ) << QgsPoint( 12, 12 ) + << QgsPoint( 10, 12 ) << QgsPoint( 10, 10 ) ); + patch.setExteriorRing( patchExterior ); + QgsLineString *patchInterior = new QgsLineString(); + patchInterior->setPoints( QgsPointSequence() << QgsPoint( 10.2, 10.2 ) + << QgsPoint( 10.9, 10.2 ) << QgsPoint( 10.9, 11.2 ) + << QgsPoint( 10.2, 11.2 ) << QgsPoint( 10.2, 10.2 ) ); + patch.addInteriorRing( patchInterior ); + polySurface.addPatch( patch.clone() ); + + + QCOMPARE( polySurface.nCoordinates(), 20 ); + QVERIFY( polySurface.insertVertex( QgsVertexId( 1, 0, 1 ), QgsPoint( 10.5, 10 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 21 ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 0 ), QgsPoint( 10, 10 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 1 ), QgsPoint( 10.5, 10 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 2 ), QgsPoint( 12, 10 ) ); + QVERIFY( polySurface.insertVertex( QgsVertexId( 1, 1, 1 ), QgsPoint( 10.8, 10.2 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 22 ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 0 ), QgsPoint( 10.2, 10.2 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 1 ), QgsPoint( 10.8, 10.2 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 2 ), QgsPoint( 10.9, 10.2 ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 1, 0, -1 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 1, 0, 100 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 1, 2, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( !polySurface.insertVertex( QgsVertexId( 2, 0, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 22 ); + + // first vertex second patch + QVERIFY( polySurface.insertVertex( QgsVertexId( 1, 0, 0 ), QgsPoint( 9, 10 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 23 ); + QCOMPARE( polySurface.patchN( 1 )->exteriorRing()->numPoints(), 7 ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 0 ), QgsPoint( 9, 10 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 1 ), QgsPoint( 10, 10 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 2 ), QgsPoint( 10.5, 10 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 3 ), QgsPoint( 12, 10 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 5 ), QgsPoint( 10, 12 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 6 ), QgsPoint( 9, 10 ) ); + + // last vertex second patch + QCOMPARE( polySurface.patchN( 1 )->interiorRing( 0 )->numPoints(), 6 ); + QVERIFY( polySurface.insertVertex( QgsVertexId( 1, 1, 6 ), QgsPoint( 0.1, 0.1 ) ) ); + QCOMPARE( polySurface.nCoordinates(), 24 ); + QCOMPARE( polySurface.patchN( 1 )->interiorRing( 0 )->numPoints(), 7 ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 0 ), QgsPoint( 0.1, 0.1 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 1 ), QgsPoint( 10.8, 10.2 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 2 ), QgsPoint( 10.9, 10.2 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 3 ), QgsPoint( 10.9, 11.2 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 5 ), QgsPoint( 10.2, 10.2 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 6 ), QgsPoint( 0.1, 0.1 ) ); +} + +void TestQgsPolyhedralSurface::testMoveVertex() +{ + // empty polyhedral surface + QgsPolyhedralSurface polySurface; + QVERIFY( !polySurface.moveVertex( QgsVertexId( 0, 0, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( polySurface.isEmpty() ); + + // valid polygon + QgsPolygon patch; + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) + << QgsPoint( 11, 12 ) << QgsPoint( 21, 22 ) + << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + QVERIFY( polySurface.moveVertex( QgsVertexId( 0, 0, 0 ), QgsPoint( 6.0, 7.0 ) ) ); + QVERIFY( polySurface.moveVertex( QgsVertexId( 0, 0, 1 ), QgsPoint( 16.0, 17.0 ) ) ); + QVERIFY( polySurface.moveVertex( QgsVertexId( 0, 0, 2 ), QgsPoint( 26.0, 27.0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 6.0, 7.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 16.0, 17.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 26.0, 27.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 6.0, 7.0 ) ); + + // move last vertex + QVERIFY( polySurface.moveVertex( QgsVertexId( 0, 0, 3 ), QgsPoint( 1.0, 2.0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 16.0, 17.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 26.0, 27.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 1.0, 2.0 ) ); + + // out of range + QVERIFY( !polySurface.moveVertex( QgsVertexId( 0, 0, -1 ), QgsPoint( 3.0, 4.0 ) ) ); + QVERIFY( !polySurface.moveVertex( QgsVertexId( 0, 0, 10 ), QgsPoint( 3.0, 4.0 ) ) ); + QVERIFY( !polySurface.moveVertex( QgsVertexId( 1, 0, 0 ), QgsPoint( 3.0, 4.0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 16.0, 17.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 26.0, 27.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 1.0, 2.0 ) ); + + // add a second patch with an interior ring + patch.clear(); + patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 10, 10 ) + << QgsPoint( 12, 10 ) << QgsPoint( 12, 12 ) + << QgsPoint( 10, 12 ) << QgsPoint( 10, 10 ) ); + patch.setExteriorRing( patchExterior ); + QgsLineString *patchInterior = new QgsLineString(); + patchInterior->setPoints( QgsPointSequence() << QgsPoint( 10.2, 10.2 ) + << QgsPoint( 10.9, 10.2 ) << QgsPoint( 10.9, 11.2 ) + << QgsPoint( 10.2, 11.2 ) << QgsPoint( 10.2, 10.2 ) ); + patch.addInteriorRing( patchInterior ); + polySurface.addPatch( patch.clone() ); + + QVERIFY( polySurface.moveVertex( QgsVertexId( 1, 0, 0 ), QgsPoint( 4.0, 5.0 ) ) ); + QVERIFY( polySurface.moveVertex( QgsVertexId( 1, 0, 1 ), QgsPoint( 14.0, 15.0 ) ) ); + QVERIFY( polySurface.moveVertex( QgsVertexId( 1, 0, 2 ), QgsPoint( 24.0, 25.0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 0 ), QgsPoint( 4.0, 5.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 1 ), QgsPoint( 14.0, 15.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 2 ), QgsPoint( 24.0, 25.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 4 ), QgsPoint( 4.0, 5.0 ) ); + + QVERIFY( polySurface.moveVertex( QgsVertexId( 1, 1, 0 ), QgsPoint( 3.0, 4.0 ) ) ); + QVERIFY( polySurface.moveVertex( QgsVertexId( 1, 1, 1 ), QgsPoint( 13.0, 14.0 ) ) ); + QVERIFY( polySurface.moveVertex( QgsVertexId( 1, 1, 2 ), QgsPoint( 23.0, 24.0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 0 ), QgsPoint( 3.0, 4.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 1 ), QgsPoint( 13.0, 14.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 2 ), QgsPoint( 23.0, 24.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 4 ), QgsPoint( 3.0, 4.0 ) ); + + // move last vertex + QVERIFY( polySurface.moveVertex( QgsVertexId( 1, 1, 4 ), QgsPoint( -1.0, -2.0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 0 ), QgsPoint( -1.0, -2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 1 ), QgsPoint( 13.0, 14.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 2 ), QgsPoint( 23.0, 24.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 4 ), QgsPoint( -1.0, -2.0 ) ); + + // out of range + QVERIFY( !polySurface.moveVertex( QgsVertexId( 1, 1, -1 ), QgsPoint( 3.0, 4.0 ) ) ); + QVERIFY( !polySurface.moveVertex( QgsVertexId( 1, 0, -1 ), QgsPoint( 3.0, 4.0 ) ) ); + QVERIFY( !polySurface.moveVertex( QgsVertexId( 1, 1, 10 ), QgsPoint( 3.0, 4.0 ) ) ); + QVERIFY( !polySurface.moveVertex( QgsVertexId( 1, 0, 10 ), QgsPoint( 3.0, 4.0 ) ) ); + QVERIFY( !polySurface.moveVertex( QgsVertexId( 2, 0, 0 ), QgsPoint( 3.0, 4.0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 0 ), QgsPoint( 4.0, 5.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 1 ), QgsPoint( 14.0, 15.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 2 ), QgsPoint( 24.0, 25.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->exteriorRing() )->pointN( 3 ), QgsPoint( 10.0, 12.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 0 ), QgsPoint( -1.0, -2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 1 ), QgsPoint( 13.0, 14.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 2 ), QgsPoint( 23.0, 24.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 1 )->interiorRing( 0 ) )->pointN( 3 ), QgsPoint( 10.2, 11.2 ) ); +} + +void TestQgsPolyhedralSurface::testDeleteVertex() +{ + // empty polygon + QgsPolyhedralSurface polySurface; + QVERIFY( !polySurface.deleteVertex( QgsVertexId( 0, 0, 0 ) ) ); + QVERIFY( !polySurface.deleteVertex( QgsVertexId( 0, 1, 0 ) ) ); + QVERIFY( polySurface.isEmpty() ); + + // valid polygon + QgsPolygon patch; + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) + << QgsPoint( 5, 2 ) << QgsPoint( 6, 2 ) + << QgsPoint( 7, 2 ) << QgsPoint( 11, 12 ) + << QgsPoint( 21, 22 ) << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + // out of range vertices + QVERIFY( !polySurface.deleteVertex( QgsVertexId( 0, 0, -1 ) ) ); + QVERIFY( !polySurface.deleteVertex( QgsVertexId( 0, 0, 100 ) ) ); + QVERIFY( !polySurface.deleteVertex( QgsVertexId( 0, 1, 1 ) ) ); + + // valid vertices + QVERIFY( polySurface.deleteVertex( QgsVertexId( 0, 0, 1 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 6.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 7.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 11.0, 12.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 5 ), QgsPoint( 1.0, 2.0 ) ); + + // delete first vertex + QVERIFY( polySurface.deleteVertex( QgsVertexId( 0, 0, 0 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 6.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 7.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 11.0, 12.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 21.0, 22.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 4 ), QgsPoint( 6.0, 2.0 ) ); + + // delete last vertex + QVERIFY( polySurface.deleteVertex( QgsVertexId( 0, 0, 4 ) ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 21.0, 22.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 1 ), QgsPoint( 7.0, 2.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 2 ), QgsPoint( 11.0, 12.0 ) ); + QCOMPARE( static_cast< const QgsLineString * >( polySurface.patchN( 0 )->exteriorRing() )->pointN( 3 ), QgsPoint( 21.0, 22.0 ) ); + + // delete another vertex - should remove patch + QVERIFY( polySurface.deleteVertex( QgsVertexId( 0, 0, 1 ) ) ); + QVERIFY( !polySurface.patchN( 0 ) ); +} + +void TestQgsPolyhedralSurface::testNextVertex() +{ + QgsPolyhedralSurface empty; + QPainter p; + empty.draw( p ); // no crash! + + QgsPoint pt; + QgsVertexId vId; + ( void )empty.closestSegment( QgsPoint( 1, 2 ), pt, vId ); // empty segment, just want no crash + + // nextVertex + QgsPolyhedralSurface surfacePoly; + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( -1, 0, 0 ); + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, -1, -1 ) ); + + vId = QgsVertexId( 0, 0, -2 ); + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( 0, 0, 10 ); + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + QgsPolygon patch; + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) + << QgsPoint( 11, 12 ) << QgsPoint( 1, 12 ) + << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( patchExterior ); + surfacePoly.addPatch( patch.clone() ); + + vId = QgsVertexId( 0, 0, 4 ); // out of range + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( 0, 0, -5 ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( 0, 0, -1 ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 0 ) ); + QCOMPARE( pt, QgsPoint( 1, 2 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 1 ) ); + QCOMPARE( pt, QgsPoint( 11, 12 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 2 ) ); + QCOMPARE( pt, QgsPoint( 1, 12 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 3 ) ); + QCOMPARE( pt, QgsPoint( 1, 2 ) ); + + vId = QgsVertexId( 0, 1, 0 ); + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( 1, 0, 0 ); + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + // add a second patch + patch.clear(); + patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) + << QgsPoint( 11, 12 ) << QgsPoint( 11, 2 ) + << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( patchExterior ); + QgsLineString *patchInterior = new QgsLineString(); + patchInterior->setPoints( QgsPointSequence() << QgsPoint( 4.5, 3 ) + << QgsPoint( 5.5, 3 ) << QgsPoint( 5, 2.5 ) + << QgsPoint( 4.5, 3 ) ); + patch.addInteriorRing( patchInterior ); + surfacePoly.addPatch( patch.clone() ); + + vId = QgsVertexId( 1, 1, 7 ); // out of range + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( 1, 0, -5 ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( 0, 0, -1 ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 0 ) ); + QCOMPARE( pt, QgsPoint( 1, 2 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 1 ) ); + QCOMPARE( pt, QgsPoint( 11, 12 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 2 ) ); + QCOMPARE( pt, QgsPoint( 1, 12 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 0, 0, 3 ) ); + QCOMPARE( pt, QgsPoint( 1, 2 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 0, 0 ) ); + QCOMPARE( pt, QgsPoint( 1, 2 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 0, 1 ) ); + QCOMPARE( pt, QgsPoint( 11, 12 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 0, 2 ) ); + QCOMPARE( pt, QgsPoint( 11, 2 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 0, 3 ) ); + QCOMPARE( pt, QgsPoint( 1, 2 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 1, 0 ) ); + QCOMPARE( pt, QgsPoint( 4.5, 3 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 1, 1 ) ); + QCOMPARE( pt, QgsPoint( 5.5, 3 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 1, 2 ) ); + QCOMPARE( pt, QgsPoint( 5, 2.5 ) ); + QVERIFY( surfacePoly.nextVertex( vId, pt ) ); + QCOMPARE( vId, QgsVertexId( 1, 1, 3 ) ); + QCOMPARE( pt, QgsPoint( 4.5, 3 ) ); + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); + + vId = QgsVertexId( 2, 0, 0 ); + QVERIFY( !surfacePoly.nextVertex( vId, pt ) ); +} + +void TestQgsPolyhedralSurface::testVertexAngle() +{ + QgsPolyhedralSurface polySurface; + + // just want no crash + ( void )polySurface.vertexAngle( QgsVertexId() ); + ( void )polySurface.vertexAngle( QgsVertexId( 0, 0, 0 ) ); + ( void )polySurface.vertexAngle( QgsVertexId( 0, 1, 0 ) ); + + QgsPolygon patch; + QgsLineString *exteriorRing = new QgsLineString; + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 0.5, 0 ) + << QgsPoint( 1, 0 ) << QgsPoint( 2, 1 ) << QgsPoint( 1, 2 ) + << QgsPoint( 0, 2 ) << QgsPoint( 0, 0 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.addPatch( patch.clone() ); + + QGSCOMPARENEAR( polySurface.vertexAngle( QgsVertexId( 0, 0, 0 ) ), 2.35619, 0.00001 ); + QGSCOMPARENEAR( polySurface.vertexAngle( QgsVertexId( 0, 0, 1 ) ), 1.5708, 0.0001 ); + QGSCOMPARENEAR( polySurface.vertexAngle( QgsVertexId( 0, 0, 2 ) ), 1.17809, 0.00001 ); + QGSCOMPARENEAR( polySurface.vertexAngle( QgsVertexId( 0, 0, 3 ) ), 0.0, 0.00001 ); + QGSCOMPARENEAR( polySurface.vertexAngle( QgsVertexId( 0, 0, 4 ) ), 5.10509, 0.00001 ); + QGSCOMPARENEAR( polySurface.vertexAngle( QgsVertexId( 0, 0, 5 ) ), 3.92699, 0.00001 ); + QGSCOMPARENEAR( polySurface.vertexAngle( QgsVertexId( 0, 0, 6 ) ), 2.35619, 0.00001 ); +} + +void TestQgsPolyhedralSurface::testDeleteVertexRemovePatch() +{ + QgsPolyhedralSurface polySurface; + + QgsPolygon *patch = new QgsPolygon(); + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) + << QgsPoint( 1, 0 ) << QgsPoint( 1, 1 ) + << QgsPoint( 0, 0 ) ); + patch->setExteriorRing( patchExterior ); + polySurface.addPatch( patch ); + + QVERIFY( polySurface.patchN( 0 ) ); + polySurface.deleteVertex( QgsVertexId( 0, 0, 2 ) ); + QVERIFY( !polySurface.patchN( 0 ) ); +} + +void TestQgsPolyhedralSurface::testVertexNumberFromVertexId() +{ + QgsPolyhedralSurface polySurface; + QgsPolygon patch; + + // with only one patch + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) + << QgsPoint( 1, 0 ) << QgsPoint( 1, 1 ) + << QgsPoint( 0, 0 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 0 ) ), 0 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 1 ) ), 1 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 2 ) ), 2 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 3 ) ), 3 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 4 ) ), -1 ); + + patch.clear(); + patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 0 ) + << QgsPoint( 1, 1 ) << QgsPoint( 2, 0 ) + << QgsPoint( 1, 0 ) ); + patch.setExteriorRing( patchExterior ); + QgsLineString *patchInterior = new QgsLineString(); + patchInterior->setPoints( QgsPointSequence() << QgsPoint( 1.2, 1.2 ) + << QgsPoint( 1.2, 1.6 ) << QgsPoint( 1.6, 1.6 ) + << QgsPoint( 1.2, 1.2 ) ); + patch.addInteriorRing( patchInterior ); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 0 ) ), 0 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 1 ) ), 1 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 2 ) ), 2 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 3 ) ), 3 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 0, 0, 4 ) ), -1 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 0, 0 ) ), 4 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 0, 1 ) ), 5 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 0, 2 ) ), 6 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 0, 3 ) ), 7 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 0, 4 ) ), -1 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 1, 0 ) ), 8 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 1, 1 ) ), 9 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 1, 2 ) ), 10 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 1, 3 ) ), 11 ); + QCOMPARE( polySurface.vertexNumberFromVertexId( QgsVertexId( 1, 1, 4 ) ), -1 ); +} + +void TestQgsPolyhedralSurface::testHasCurvedSegments() +{ + QgsPolyhedralSurface polySurface; + QVERIFY( !polySurface.hasCurvedSegments() ); + + QgsPolygon patch; + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) + << QgsPoint( 11, 12 ) << QgsPoint( 21, 22 ) + << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + QVERIFY( !polySurface.hasCurvedSegments() ); +} + +void TestQgsPolyhedralSurface::testClosestSegment() +{ + QgsPolyhedralSurface empty; + QPainter p; + empty.draw( p ); // no crash! + + QgsPoint pt; + QgsVertexId v; + int leftOf = 0; + ( void )empty.closestSegment( QgsPoint( 1, 2 ), pt, v ); // empty segment, just want no crash + + QgsPolyhedralSurface polySurface; + QgsPolygon patch; + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 5, 10 ) + << QgsPoint( 7, 12 ) << QgsPoint( 5, 15 ) + << QgsPoint( 5, 10 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 4, 11 ), pt, v, &leftOf ), 1.0, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 5, 0.01 ); + QGSCOMPARENEAR( pt.y(), 11, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 3 ) ); + QCOMPARE( leftOf, 1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 8, 11 ), pt, v, &leftOf ), 2.0, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 7, 0.01 ); + QGSCOMPARENEAR( pt.y(), 12, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 1 ) ); + QCOMPARE( leftOf, 1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 6, 11.5 ), pt, v, &leftOf ), 0.125000, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 6.25, 0.01 ); + QGSCOMPARENEAR( pt.y(), 11.25, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 1 ) ); + QCOMPARE( leftOf, -1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 2.5, 13 ), pt, v, &leftOf ), 6.25000, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 5.0, 0.01 ); + QGSCOMPARENEAR( pt.y(), 13.0, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 3 ) ); + QCOMPARE( leftOf, 1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 5.5, 13.5 ), pt, v, &leftOf ), 0.173077, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 5.846154, 0.01 ); + QGSCOMPARENEAR( pt.y(), 13.730769, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 2 ) ); + QCOMPARE( leftOf, -1 ); + + // point directly on segment + QCOMPARE( polySurface.closestSegment( QgsPoint( 5, 15 ), pt, v, &leftOf ), 0.0 ); + QCOMPARE( pt, QgsPoint( 5, 15 ) ); + QCOMPARE( v, QgsVertexId( 0, 0, 2 ) ); + QCOMPARE( leftOf, 0 ); + + // with a second patch + patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 5, 10 ) + << QgsPoint( 5, 15 ) << QgsPoint( 2, 11 ) + << QgsPoint( 5, 10 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 4, 11 ), pt, v, &leftOf ), 0.4, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 3.8, 0.01 ); + QGSCOMPARENEAR( pt.y(), 10.4, 0.01 ); + QCOMPARE( v, QgsVertexId( 1, 0, 3 ) ); + QCOMPARE( leftOf, -1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 8, 11 ), pt, v, &leftOf ), 2.0, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 7, 0.01 ); + QGSCOMPARENEAR( pt.y(), 12, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 1 ) ); + QCOMPARE( leftOf, 1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 6, 11.5 ), pt, v, &leftOf ), 0.125000, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 6.25, 0.01 ); + QGSCOMPARENEAR( pt.y(), 11.25, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 1 ) ); + QCOMPARE( leftOf, -1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 2.5, 13 ), pt, v, &leftOf ), 0.64000, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 3.14, 0.01 ); + QGSCOMPARENEAR( pt.y(), 12.52, 0.01 ); + QCOMPARE( v, QgsVertexId( 1, 0, 2 ) ); + QCOMPARE( leftOf, 1 ); + + QGSCOMPARENEAR( polySurface.closestSegment( QgsPoint( 5.5, 13.5 ), pt, v, &leftOf ), 0.173077, 0.0001 ); + QGSCOMPARENEAR( pt.x(), 5.846154, 0.01 ); + QGSCOMPARENEAR( pt.y(), 13.730769, 0.01 ); + QCOMPARE( v, QgsVertexId( 0, 0, 2 ) ); + QCOMPARE( leftOf, -1 ); + + // point directly on segment + QCOMPARE( polySurface.closestSegment( QgsPoint( 2, 11 ), pt, v, &leftOf ), 0.0 ); + QCOMPARE( pt, QgsPoint( 2, 11 ) ); + QCOMPARE( v, QgsVertexId( 1, 0, 2 ) ); + QCOMPARE( leftOf, 0 ); +} + +void TestQgsPolyhedralSurface::testBoundary() +{ + QgsPolygon patch; + QgsPolyhedralSurface polySurface; + QVERIFY( !polySurface.boundary() ); + + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 0, 0, 1 ) + << QgsPoint( 1, 0, 2 ) << QgsPoint( 2, 0, 3 ) << QgsPoint( 1, 0.5, 4 ) + << QgsPoint( 0, 0, 1 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + QgsAbstractGeometry *boundary = polySurface.boundary(); + QgsMultiLineString *multiLineBoundary = dynamic_cast< QgsMultiLineString * >( boundary ); + QVERIFY( multiLineBoundary ); + QCOMPARE( multiLineBoundary->numGeometries(), 1 ); + QgsLineString *lineBoundary = multiLineBoundary->lineStringN( 0 ); + QCOMPARE( lineBoundary->numPoints(), 5 ); + QCOMPARE( lineBoundary->xAt( 0 ), 0.0 ); + QCOMPARE( lineBoundary->xAt( 1 ), 1.0 ); + QCOMPARE( lineBoundary->xAt( 2 ), 2.0 ); + QCOMPARE( lineBoundary->xAt( 3 ), 1.0 ); + QCOMPARE( lineBoundary->xAt( 4 ), 0.0 ); + QCOMPARE( lineBoundary->yAt( 0 ), 0.0 ); + QCOMPARE( lineBoundary->yAt( 1 ), 0.0 ); + QCOMPARE( lineBoundary->yAt( 2 ), 0.0 ); + QCOMPARE( lineBoundary->yAt( 3 ), 0.5 ); + QCOMPARE( lineBoundary->yAt( 4 ), 0.0 ); + delete boundary; + + QgsLineString *patchInterior1 = new QgsLineString(); + patchInterior1->setPoints( QgsPointSequence() << QgsPoint( 0.1, 0.1 ) + << QgsPoint( 0.2, 0.1 ) << QgsPoint( 0.2, 0.2 ) ); + patch.addInteriorRing( patchInterior1 ); + QgsLineString *patchInterior2 = new QgsLineString(); + patchInterior2->setPoints( QgsPointSequence() << QgsPoint( 0.8, 0.8 ) + << QgsPoint( 0.9, 0.8 ) << QgsPoint( 0.9, 0.9 ) ); + patch.addInteriorRing( patchInterior2 ); + + + polySurface.removePatch( 0 ); + QCOMPARE( polySurface.numPatches(), 0 ); + polySurface.addPatch( patch.clone() ); + boundary = polySurface.boundary(); + + multiLineBoundary = dynamic_cast< QgsMultiLineString * >( boundary ); + QVERIFY( multiLineBoundary ); + QCOMPARE( multiLineBoundary->numGeometries(), 3 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->numPoints(), 5 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->xAt( 0 ), 0.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->xAt( 1 ), 1.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->xAt( 2 ), 2.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->xAt( 3 ), 1.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->xAt( 4 ), 0.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->yAt( 0 ), 0.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->yAt( 1 ), 0.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->yAt( 2 ), 0.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->yAt( 3 ), 0.5 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 0 ) )->yAt( 4 ), 0.0 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->numPoints(), 4 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->xAt( 0 ), 0.1 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->xAt( 1 ), 0.2 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->xAt( 2 ), 0.2 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->xAt( 3 ), 0.1 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->yAt( 0 ), 0.1 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->yAt( 1 ), 0.1 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->yAt( 2 ), 0.2 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 1 ) )->yAt( 3 ), 0.1 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->numPoints(), 4 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->xAt( 0 ), 0.8 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->xAt( 1 ), 0.9 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->xAt( 2 ), 0.9 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->xAt( 3 ), 0.8 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->yAt( 0 ), 0.8 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->yAt( 1 ), 0.8 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->yAt( 2 ), 0.9 ); + QCOMPARE( qgis::down_cast< QgsLineString * >( multiLineBoundary->geometryN( 2 ) )->yAt( 3 ), 0.8 ); + + polySurface.removePatch( 0 ); + QCOMPARE( polySurface.numPatches(), 0 ); + patch.removeInteriorRings(); + delete boundary; + + // test boundary with z + patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 10 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 0, 15 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 1, 20 ) ); + patch.setExteriorRing( patchExterior ); + polySurface.addPatch( patch.clone() ); + + boundary = polySurface.boundary(); + multiLineBoundary = dynamic_cast< QgsMultiLineString * >( boundary ); + QVERIFY( multiLineBoundary ); + QCOMPARE( multiLineBoundary->numGeometries(), 1 ); + lineBoundary = multiLineBoundary->lineStringN( 0 ); + QCOMPARE( lineBoundary->numPoints(), 4 ); + QCOMPARE( lineBoundary->wkbType(), Qgis::WkbType::LineStringZ ); + QCOMPARE( lineBoundary->pointN( 0 ).z(), 10.0 ); + QCOMPARE( lineBoundary->pointN( 1 ).z(), 15.0 ); + QCOMPARE( lineBoundary->pointN( 2 ).z(), 20.0 ); + QCOMPARE( lineBoundary->pointN( 3 ).z(), 10.0 ); + + delete boundary; +} + +void TestQgsPolyhedralSurface::testBoundingBox() +{ + QgsPolyhedralSurface polySurface; + QgsRectangle bBox = polySurface.boundingBox(); // no crash! + + QgsPolygon *patch1 = new QgsPolygon(); + QgsLineString *patchExterior1 = new QgsLineString(); + patchExterior1->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 1, 2, 3 ) + << QgsPoint( Qgis::WkbType::PointZ, 4, 5, 6 ) << QgsPoint( Qgis::WkbType::PointZ, 7, 8, 9 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 2, 3 ) ); + patch1->setExteriorRing( patchExterior1 ); + polySurface.addPatch( patch1 ); + + QgsPolygon *patch2 = new QgsPolygon(); + QgsLineString *patchExterior2 = new QgsLineString(); + patchExterior2->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 10, 11, 12 ) + << QgsPoint( Qgis::WkbType::PointZ, 13, 14, 15 ) << QgsPoint( Qgis::WkbType::PointZ, 16, 17, 18 ) + << QgsPoint( Qgis::WkbType::PointZ, 10, 11, 12 ) ); + patch2->setExteriorRing( patchExterior2 ); + QgsLineString *patchInterior2 = new QgsLineString(); + patchInterior2->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 10.5, 11.5, 12.5 ) + << QgsPoint( Qgis::WkbType::PointZ, 13.5, 14.5, 15.5 ) << QgsPoint( Qgis::WkbType::PointZ, 15.5, 16.5, 17.5 ) + << QgsPoint( Qgis::WkbType::PointZ, 10.5, 11.5, 12.5 ) ); + patch2->addInteriorRing( patchInterior2 ); + polySurface.addPatch( patch2 ); + + bBox = polySurface.boundingBox(); + QGSCOMPARENEAR( bBox.xMinimum(), 1.0, 0.001 ); + QGSCOMPARENEAR( bBox.xMaximum(), 16.0, 0.001 ); + QGSCOMPARENEAR( bBox.yMinimum(), 2.0, 0.001 ); + QGSCOMPARENEAR( bBox.yMaximum(), 17.0, 0.001 ); +} + +void TestQgsPolyhedralSurface::testBoundingBox3D() +{ + QgsPolyhedralSurface polySurface; + QgsBox3D bBox = polySurface.boundingBox3D(); + QCOMPARE( bBox.xMinimum(), std::numeric_limits::quiet_NaN() ); + QCOMPARE( bBox.xMaximum(), std::numeric_limits::quiet_NaN() ); + QCOMPARE( bBox.yMinimum(), std::numeric_limits::quiet_NaN() ); + QCOMPARE( bBox.yMaximum(), std::numeric_limits::quiet_NaN() ); + QCOMPARE( bBox.zMinimum(), std::numeric_limits::quiet_NaN() ); + QCOMPARE( bBox.zMaximum(), std::numeric_limits::quiet_NaN() ); + + QgsPolygon *patch = new QgsPolygon(); + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 6 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 10, 2 ) << QgsPoint( Qgis::WkbType::PointZ, 0, 18, 3 ) + << QgsPoint( Qgis::WkbType::PointZ, -1, 4, 4 ) << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 6 ) ); + patch->setExteriorRing( patchExterior ); + polySurface.addPatch( patch ); + + bBox = polySurface.boundingBox3D(); + QGSCOMPARENEAR( bBox.xMinimum(), -1.0, 0.001 ); + QGSCOMPARENEAR( bBox.xMaximum(), 1.0, 0.001 ); + QGSCOMPARENEAR( bBox.yMinimum(), 0.0, 0.001 ); + QGSCOMPARENEAR( bBox.yMaximum(), 18.0, 0.001 ); + QGSCOMPARENEAR( bBox.zMinimum(), 2.0, 0.001 ); + QGSCOMPARENEAR( bBox.zMaximum(), 6.0, 0.001 ); +} + +void TestQgsPolyhedralSurface::testBoundingBoxIntersects() +{ + // 2d + QgsPolyhedralSurface polySurface1; + QVERIFY( !polySurface1.boundingBoxIntersects( QgsRectangle( 1, 3, 6, 9 ) ) ); + + QgsPolygon *patch1 = new QgsPolygon(); + QgsLineString *patchExterior1 = new QgsLineString(); + patchExterior1->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 1 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 10, 2 ) << QgsPoint( Qgis::WkbType::PointZ, 0, 18, 3 ) + << QgsPoint( Qgis::WkbType::PointZ, -1, 4, 4 ) << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 1 ) ); + patch1->setExteriorRing( patchExterior1 ); + polySurface1.addPatch( patch1 ); + + QVERIFY( polySurface1.boundingBoxIntersects( QgsRectangle( 1, 3, 6, 9 ) ) ); + QVERIFY( !polySurface1.boundingBoxIntersects( QgsRectangle( 1.1, -5, 6, -2 ) ) ); + + // 3d + QgsPolyhedralSurface polySurface2; + QVERIFY( !polySurface2.boundingBoxIntersects( QgsBox3D( 1, 3, 1, 6, 9, 2 ) ) ); + + QgsPolygon *patch2 = new QgsPolygon(); + QgsLineString *patchExterior2 = new QgsLineString(); + patchExterior2->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 1 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 10, 2 ) << QgsPoint( Qgis::WkbType::PointZ, 0, 18, 3 ) + << QgsPoint( Qgis::WkbType::PointZ, -1, 4, 4 ) << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 1 ) ); + patch2->setExteriorRing( patchExterior2 ); + polySurface2.addPatch( patch2 ); + + QVERIFY( polySurface2.boundingBoxIntersects( QgsBox3D( 1, 3, 1, 6, 9, 2 ) ) ); + QVERIFY( !polySurface2.boundingBoxIntersects( QgsBox3D( 1, 3, 4.1, 6, 9, 6 ) ) ); +} + +void TestQgsPolyhedralSurface::testDropZValue() +{ + QgsPolyhedralSurface polySurface; + QgsPolygon patch; + + // without z + polySurface.dropZValue(); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + + QgsLineString *exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) + << QgsPoint( 1, 4 ) << QgsPoint( 4, 4 ) + << QgsPoint( 4, 1 ) << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.addPatch( patch.clone() ); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + + polySurface.dropZValue(); // not z + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::Polygon ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1, 2 ) ); + + // with z + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 10, 20, 3 ) << QgsPoint( 11, 12, 13 ) + << QgsPoint( 1, 12, 23 ) << QgsPoint( 10, 20, 3 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.clear(); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceZ ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::PolygonZ ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 10, 20, 3 ) ); + + polySurface.dropZValue(); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::Polygon ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 10, 20 ) ); + + // with zm + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 1, 2, 3, 4 ) << QgsPoint( 11, 12, 13, 14 ) + << QgsPoint( 1, 12, 23, 24 ) << QgsPoint( 1, 2, 3, 4 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.clear(); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceZM ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::PolygonZM ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( Qgis::WkbType::PointZM, 1, 2, 3, 4 ) ); + + polySurface.dropZValue(); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceM ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::PolygonM ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( Qgis::WkbType::PointM, 1, 2, 0, 4 ) ); +} + +void TestQgsPolyhedralSurface::testDropMValue() +{ + QgsPolyhedralSurface polySurface; + QgsPolygon patch; + + // without z + polySurface.dropMValue(); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + + QgsLineString *exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) << QgsPoint( 11, 12 ) + << QgsPoint( 1, 12 ) << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1, 2 ) ); + + polySurface.dropMValue(); // not zm + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::Polygon ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1, 2 ) ); + + // with m + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointM, 1, 2, 0, 3 ) + << QgsPoint( Qgis::WkbType::PointM, 11, 12, 0, 13 ) + << QgsPoint( Qgis::WkbType::PointM, 1, 12, 0, 23 ) + << QgsPoint( Qgis::WkbType::PointM, 1, 2, 0, 3 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.clear(); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceM ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::PolygonM ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( Qgis::WkbType::PointM, 1, 2, 0, 3 ) ); + + polySurface.dropMValue(); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::Polygon ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1, 2 ) ); + + // with zm + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 1, 2, 3, 4 ) << QgsPoint( 11, 12, 13, 14 ) + << QgsPoint( 1, 12, 23, 24 ) << QgsPoint( 1, 2, 3, 4 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.clear(); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceZM ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::PolygonZM ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( 1, 2, 3, 4 ) ); + + polySurface.dropMValue(); + QCOMPARE( polySurface.wkbType(), Qgis::WkbType::PolyhedralSurfaceZ ); + QCOMPARE( polySurface.patchN( 0 )->wkbType(), Qgis::WkbType::PolygonZ ); + QCOMPARE( static_cast< const QgsLineString *>( polySurface.patchN( 0 )->exteriorRing() )->pointN( 0 ), QgsPoint( Qgis::WkbType::PointZ, 1, 2, 3 ) ); +} + +void TestQgsPolyhedralSurface::testCoordinateSequence() +{ + QgsPolyhedralSurface surfacePoly; + QgsPolygon patch; + + QgsLineString *patchExterior = new QgsLineString(); + patchExterior->setPoints( QgsPointSequence() << QgsPoint( 1, 2 ) + << QgsPoint( 11, 12 ) << QgsPoint( 11, 2 ) + << QgsPoint( 1, 2 ) ); + patch.setExteriorRing( patchExterior ); + QgsLineString *patchInterior = new QgsLineString(); + patchInterior->setPoints( QgsPointSequence() << QgsPoint( 4.5, 3 ) + << QgsPoint( 5.5, 3 ) << QgsPoint( 5, 2.5 ) + << QgsPoint( 4.5, 3 ) ); + patch.addInteriorRing( patchInterior ); + surfacePoly.addPatch( patch.clone() ); + + QgsCoordinateSequence coordinateSequence = surfacePoly.coordinateSequence(); + QgsCoordinateSequence expectedSequence; + expectedSequence << ( QgsRingSequence() << ( QgsPointSequence() << QgsPoint( 1, 2 ) << QgsPoint( 11, 12 ) << QgsPoint( 11, 2 ) << QgsPoint( 1, 2 ) ) + << ( QgsPointSequence() << QgsPoint( 4.5, 3 ) << QgsPoint( 5.5, 3 ) << QgsPoint( 5, 2.5 ) << QgsPoint( 4.5, 3 ) ) ); + QCOMPARE( coordinateSequence, expectedSequence ); +} + +void TestQgsPolyhedralSurface::testChildGeometry() +{ + // childCount and childGeometry are protected method + // use this intermediate class to test them + class QgsPolyhedralSurfaceWithChildPublic : public QgsPolyhedralSurface + { + public: + using QgsPolyhedralSurface::childCount; + using QgsPolyhedralSurface::childGeometry; + }; + + QgsPolyhedralSurfaceWithChildPublic polySurface; + QCOMPARE( polySurface.childCount(), 0 ); + + QgsPolygon patch; + QgsLineString *exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 1, 0 ) + << QgsPoint( 2, 0 ) << QgsPoint( 1, 0.5 ) << QgsPoint( 0, 0 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface.addPatch( patch.clone() ); + + QCOMPARE( polySurface.childCount(), 1 ); + QVERIFY( polySurface.childGeometry( 0 ) ); +} + +void TestQgsPolyhedralSurface::testWKB() +{ + QgsPolyhedralSurface polySurface1; + QgsPolyhedralSurface polySurface2; + QgsPolygon patch; + QgsLineString *exteriorRing; + QgsLineString *interiorRing; + + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 1, 0 ) + << QgsPoint( 2, 0 ) << QgsPoint( 1, 0.5 ) << QgsPoint( 0, 0 ) ); + patch.setExteriorRing( exteriorRing ); + polySurface1.addPatch( patch.clone() ); + + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 0.1, 0 ) + << QgsPoint( 0.2, 0 ) << QgsPoint( 0.1, 0.05 ) << QgsPoint( 0, 0 ) ); + patch.clear(); + patch.setExteriorRing( exteriorRing ); + interiorRing = new QgsLineString(); + interiorRing->setPoints( QgsPointSequence() << QgsPoint( 0.02, 0.02 ) << QgsPoint( 0.06, 0.02 ) + << QgsPoint( 0.06, 0.04 ) << QgsPoint( 0.02, 0.02 ) ); + patch.addInteriorRing( interiorRing ); + polySurface1.addPatch( patch.clone() ); + + QCOMPARE( polySurface1.wkbType(), Qgis::WkbType::PolyhedralSurface ); + QByteArray wkb16 = polySurface1.asWkb(); + QCOMPARE( wkb16.size(), polySurface1.wkbSize() ); + + QgsConstWkbPtr wkb16ptr( wkb16 ); + polySurface2.fromWkb( wkb16ptr ); + QCOMPARE( polySurface1, polySurface2 ); + + polySurface1.clear(); + polySurface2.clear(); + + // PolyhedralSurfaceZ + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0, 1 ) << QgsPoint( 1, 0, 2 ) + << QgsPoint( 2, 0, 3 ) << QgsPoint( 1, 0.5, 4 ) << QgsPoint( 0, 0, 1 ) ); + patch.clear(); + patch.setExteriorRing( exteriorRing ); + polySurface1.addPatch( patch.clone() ); + + patch.clear(); + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0, 1 ) << QgsPoint( 0.1, 0, 2 ) + << QgsPoint( 0.2, 0, 3 ) << QgsPoint( 0.1, 0.05, 4 ) << QgsPoint( 0, 0, 1 ) ); + interiorRing = new QgsLineString(); + interiorRing->setPoints( QgsPointSequence() << QgsPoint( 0.02, 0.02, 1 ) << QgsPoint( 0.06, 0.02, 1 ) + << QgsPoint( 0.06, 0.04, 1 ) << QgsPoint( 0.02, 0.02, 1 ) ); + patch.setExteriorRing( exteriorRing ); + patch.addInteriorRing( interiorRing ); + polySurface1.addPatch( patch.clone() ); + + QCOMPARE( polySurface1.wkbType(), Qgis::WkbType::PolyhedralSurfaceZ ); + wkb16 = polySurface1.asWkb(); + QgsConstWkbPtr wkb16ptr2( wkb16 ); + polySurface2.fromWkb( wkb16ptr2 ); + QCOMPARE( polySurface1, polySurface2 ); + + polySurface1.clear(); + polySurface2.clear(); + + // PolyhedralSurfaceM + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointM, 0, 0, 0, 1 ) + << QgsPoint( Qgis::WkbType::PointM, 1, 0, 0, 2 ) << QgsPoint( Qgis::WkbType::PointM, 2, 0, 0, 3 ) + << QgsPoint( Qgis::WkbType::PointM, 1, 0.5, 0, 4 ) << QgsPoint( Qgis::WkbType::PointM, 0, 0, 0, 1 ) ); + patch.clear(); + patch.setExteriorRing( exteriorRing ); + polySurface1.addPatch( patch.clone() ); + + patch.clear(); + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointM, 0, 0, 0, 1 ) + << QgsPoint( Qgis::WkbType::PointM, 0.1, 0, 0, 2 ) << QgsPoint( Qgis::WkbType::PointM, 0.2, 0, 0, 3 ) + << QgsPoint( Qgis::WkbType::PointM, 0.1, 0.05, 0, 4 ) << QgsPoint( Qgis::WkbType::PointM, 0, 0, 0, 1 ) ); + patch.setExteriorRing( exteriorRing ); + interiorRing = new QgsLineString(); + interiorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointM, 0.02, 0.02, 0, 1 ) << QgsPoint( Qgis::WkbType::PointM, 0.06, 0.02, 0, 1 ) + << QgsPoint( Qgis::WkbType::PointM, 0.06, 0.04, 0, 1 ) << QgsPoint( Qgis::WkbType::PointM, 0.02, 0.02, 0, 1 ) ); + patch.addInteriorRing( interiorRing ); + polySurface1.addPatch( patch.clone() ); + + QCOMPARE( polySurface1.wkbType(), Qgis::WkbType::PolyhedralSurfaceM ); + wkb16 = polySurface1.asWkb(); + QgsConstWkbPtr wkb16ptr4( wkb16 ); + polySurface2.fromWkb( wkb16ptr4 ); + QCOMPARE( polySurface1, polySurface2 ); + + polySurface1.clear(); + polySurface2.clear(); + + // PolyhedralSurfaceZM + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) + << QgsPoint( Qgis::WkbType::PointZM, 1, 0, 11, 2 ) << QgsPoint( Qgis::WkbType::PointZM, 2, 0, 12, 3 ) + << QgsPoint( Qgis::WkbType::PointZM, 1, 0.5, 13, 4 ) << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) ); + patch.clear(); + patch.setExteriorRing( exteriorRing ); + polySurface1.addPatch( patch.clone() ); + + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) + << QgsPoint( Qgis::WkbType::PointZM, 0.1, 0, 11, 2 ) << QgsPoint( Qgis::WkbType::PointZM, 0.2, 0, 12, 3 ) + << QgsPoint( Qgis::WkbType::PointZM, 0.1, 0.05, 13, 4 ) << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) ); + patch.clear(); + patch.setExteriorRing( exteriorRing ); + interiorRing = new QgsLineString(); + interiorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 0.02, 0.02, 10, 1 ) << QgsPoint( Qgis::WkbType::PointZM, 0.06, 0.02, 10, 1 ) + << QgsPoint( Qgis::WkbType::PointZM, 0.06, 0.04, 10, 1 ) << QgsPoint( Qgis::WkbType::PointZM, 0.02, 0.02, 10, 1 ) ); + patch.addInteriorRing( interiorRing ); + polySurface1.addPatch( patch.clone() ); + + QCOMPARE( polySurface1.wkbType(), Qgis::WkbType::PolyhedralSurfaceZM ); + wkb16 = polySurface1.asWkb(); + QgsConstWkbPtr wkb16ptr5( wkb16 ); + polySurface2.fromWkb( wkb16ptr5 ); + QCOMPARE( polySurface1, polySurface2 ); + + polySurface1.clear(); + polySurface2.clear(); + + // bad WKB - check for no crash + QgsConstWkbPtr nullPtr( nullptr, 0 ); + QVERIFY( !polySurface2.fromWkb( nullPtr ) ); + QCOMPARE( polySurface2.wkbType(), Qgis::WkbType::PolyhedralSurface ); + + QgsPoint point( 1, 2 ); + QByteArray wkbPoint = point.asWkb(); + QgsConstWkbPtr wkbPointPtr( wkbPoint ); + + QVERIFY( !polySurface2.fromWkb( wkbPointPtr ) ); + QCOMPARE( polySurface2.wkbType(), Qgis::WkbType::PolyhedralSurface ); + + // GeoJSON export + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0, 1 ) << QgsPoint( 1, 0, 2 ) + << QgsPoint( 2, 0, 3 ) << QgsPoint( 1, 0.5, 4 ) << QgsPoint( 0, 0, 1 ) ); + patch.clear(); + patch.setExteriorRing( exteriorRing ); + polySurface1.addPatch( patch.clone() ); + + patch.clear(); + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( 0, 0, 1 ) << QgsPoint( 0.1, 0, 2 ) + << QgsPoint( 0.2, 0, 3 ) << QgsPoint( 0.1, 0.05, 4 ) << QgsPoint( 0, 0, 1 ) ); + interiorRing = new QgsLineString(); + interiorRing->setPoints( QgsPointSequence() << QgsPoint( 0.02, 0.02, 1 ) << QgsPoint( 0.06, 0.02, 1 ) + << QgsPoint( 0.06, 0.04, 1 ) << QgsPoint( 0.02, 0.02, 1 ) ); + patch.setExteriorRing( exteriorRing ); + patch.addInteriorRing( interiorRing ); + polySurface1.addPatch( patch.clone() ); + + QString expectedSimpleJson( "{\"coordinates\":[[[[0.0,0.0,1.0],[1.0,0.0,2.0],[2.0,0.0,3.0],[1.0,0.5,4.0],[0.0,0.0,1.0]]],[[[0.0,0.0,1.0],[0.1,0.0,2.0],[0.2,0.0,3.0],[0.1,0.05,4.0],[0.0,0.0,1.0]],[[0.02,0.02,1.0],[0.06,0.02,1.0],[0.06,0.04,1.0],[0.02,0.02,1.0]]]],\"type\":\"MultiPolygon\"}" ); + QString jsonRes = polySurface1.asJson( 2 ); + QCOMPARE( jsonRes, expectedSimpleJson ); + +} + +void TestQgsPolyhedralSurface::testWKT() +{ + QgsPolyhedralSurface polySurface1; + QgsPolygon patch; + QgsLineString *exteriorRing; + QgsLineString *interiorRing; + + exteriorRing = new QgsLineString(); + exteriorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) + << QgsPoint( Qgis::WkbType::PointZM, 0.1, 0, 11, 2 ) << QgsPoint( Qgis::WkbType::PointZM, 0.2, 0, 12, 3 ) + << QgsPoint( Qgis::WkbType::PointZM, 0.1, 0.05, 13, 4 ) << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) ); + patch.setExteriorRing( exteriorRing ); + interiorRing = new QgsLineString(); + interiorRing->setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 0.02, 0.02, 10, 1 ) << QgsPoint( Qgis::WkbType::PointZM, 0.06, 0.02, 10, 1 ) + << QgsPoint( Qgis::WkbType::PointZM, 0.06, 0.04, 10, 1 ) << QgsPoint( Qgis::WkbType::PointZM, 0.02, 0.02, 10, 1 ) ); + patch.addInteriorRing( interiorRing ); + polySurface1.addPatch( patch.clone() ); + + QString wkt = polySurface1.asWkt(); + + QgsPolyhedralSurface polySurface2; + QVERIFY( polySurface2.fromWkt( wkt ) ); + QCOMPARE( polySurface1, polySurface2 ); + + // bad WKT + QVERIFY( !polySurface2.fromWkt( "Point()" ) ); + QVERIFY( polySurface2.isEmpty() ); + QVERIFY( !polySurface2.patchN( 0 ) ); + QCOMPARE( polySurface2.numPatches(), 0 ); + QVERIFY( !polySurface2.is3D() ); + QVERIFY( !polySurface2.isMeasure() ); + QCOMPARE( polySurface2.wkbType(), Qgis::WkbType::PolyhedralSurface ); +} + +void TestQgsPolyhedralSurface::testExport() +{ + QgsPolyhedralSurface exportPolygon; + QgsPolygon patch; + QgsLineString exteriorRing; + QgsLineString interiorRing; + QString expectedSimpleGML3; + QString result; + + // GML document for compare + QDomDocument doc( QStringLiteral( "gml" ) ); + + // Z + // as GML3 - M is dropped + exteriorRing.setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 10 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 0, 11 ) << QgsPoint( Qgis::WkbType::PointZ, 2, 0, 12 ) + << QgsPoint( Qgis::WkbType::PointZ, 1, 0.5, 13 ) << QgsPoint( Qgis::WkbType::PointZ, 0, 0, 10 ) ); + patch.setExteriorRing( exteriorRing.clone() ); + interiorRing.setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZ, 0.02, 0.02, 10 ) << QgsPoint( Qgis::WkbType::PointZ, 0.06, 0.02, 10 ) << QgsPoint( Qgis::WkbType::PointZ, 0.06, 0.04, 10 ) << QgsPoint( Qgis::WkbType::PointZ, 0.02, 0.02, 10 ) ); + patch.addInteriorRing( interiorRing.clone() ); + exportPolygon.addPatch( patch.clone() ); + + expectedSimpleGML3 = QString( QStringLiteral( "0 0 10 1 0 11 2 0 12 1 0.5 13 0 0 100.02 0.02 10 0.06 0.02 10 0.06 0.04 10 0.02 0.02 10" ) ); + result = elemToString( exportPolygon.asGml3( doc, 2 ) ); + QCOMPARE( elemToString( exportPolygon.asGml3( doc ) ), expectedSimpleGML3 ); + + // ZM + // as GML3 + exportPolygon.clear(); + patch.clear(); + exteriorRing.setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) + << QgsPoint( Qgis::WkbType::PointZM, 1, 0, 11, 2 ) << QgsPoint( Qgis::WkbType::PointZM, 2, 0, 12, 3 ) + << QgsPoint( Qgis::WkbType::PointZM, 1, 0.5, 13, 4 ) << QgsPoint( Qgis::WkbType::PointZM, 0, 0, 10, 1 ) ); + patch.setExteriorRing( exteriorRing.clone() ); + interiorRing.setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 0.02, 0.02, 10, 1 ) << QgsPoint( Qgis::WkbType::PointZM, 0.06, 0.02, 10, 1 ) << QgsPoint( Qgis::WkbType::PointZM, 0.06, 0.04, 10, 1 ) << QgsPoint( Qgis::WkbType::PointZM, 0.02, 0.02, 10, 1 ) ); + patch.addInteriorRing( interiorRing.clone() ); + exportPolygon.addPatch( patch.clone() ); + + expectedSimpleGML3 = QString( QStringLiteral( "0 0 10 1 0 11 2 0 12 1 0.5 13 0 0 100.02 0.02 10 0.06 0.02 10 0.06 0.04 10 0.02 0.02 10" ) ); + result = elemToString( exportPolygon.asGml3( doc, 2 ) ); + QCOMPARE( elemToString( exportPolygon.asGml3( doc ) ), expectedSimpleGML3 ); + + QString expectedGML3empty( QStringLiteral( "" ) ); + QGSCOMPAREGML( elemToString( QgsPolyhedralSurface().asGml3( doc ) ), expectedGML3empty ); +} + +void TestQgsPolyhedralSurface::testCast() +{ + QVERIFY( !QgsPolyhedralSurface().cast( nullptr ) ); + + QgsPolyhedralSurface pCast; + QVERIFY( QgsPolyhedralSurface().cast( &pCast ) ); + + QgsPolyhedralSurface pCast2; + pCast2.fromWkt( QStringLiteral( "PolyhedralSurfaceZ((0 0 0, 0 1 1, 1 0 2, 0 0 0))" ) ); + QVERIFY( QgsPolyhedralSurface().cast( &pCast2 ) ); + + pCast2.fromWkt( QStringLiteral( "PolyhedralSurfaceM((0 0 1, 0 1 2, 1 0 3, 0 0 1))" ) ); + QVERIFY( QgsPolyhedralSurface().cast( &pCast2 ) ); + + pCast2.fromWkt( QStringLiteral( "PolyhedralSurfaceZM((0 0 0 1, 0 1 1 2, 1 0 2 3, 0 0 0 1))" ) ); + QVERIFY( QgsPolyhedralSurface().cast( &pCast2 ) ); + + QVERIFY( !pCast2.fromWkt( QStringLiteral( "PolyhedralSurfaceZ((111111))" ) ) ); +} + + +QGSTEST_MAIN( TestQgsPolyhedralSurface ) +#include "testqgspolyhedralsurface.moc" diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index dfff3ca6ec0..5cbbc6e6ed1 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -233,6 +233,7 @@ ADD_PYTHON_TEST(PyQgsPointCloudRgbRenderer test_qgspointcloudrgbrenderer.py) ADD_PYTHON_TEST(PyQgsPointClusterRenderer test_qgspointclusterrenderer.py) ADD_PYTHON_TEST(PyQgsPointDisplacementRenderer test_qgspointdisplacementrenderer.py) ADD_PYTHON_TEST(PyQgsPolygon test_qgspolygon.py) +ADD_PYTHON_TEST(PyQgsPolyhedralSurface test_qgspolyhedralsurface.py) ADD_PYTHON_TEST(PyQgsProcessExecutablePt1 test_qgsprocessexecutable_pt1.py) ADD_PYTHON_TEST(PyQgsProcessExecutablePt2 test_qgsprocessexecutable_pt2.py) ADD_PYTHON_TEST(PyQgsProcessingInPlace test_qgsprocessinginplace.py) diff --git a/tests/src/python/test_qgspolyhedralsurface.py b/tests/src/python/test_qgspolyhedralsurface.py new file mode 100644 index 00000000000..dd54fa6d42d --- /dev/null +++ b/tests/src/python/test_qgspolyhedralsurface.py @@ -0,0 +1,194 @@ +"""QGIS Unit tests for QgsPolyhedralSurface + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = 'Jean Felder' +__date__ = '12/08/2024' +__copyright__ = 'Copyright 2024, The QGIS Project' + +import qgis # NOQA + +from qgis.core import ( + QgsLineString, QgsPoint, QgsPolygon, QgsPolyhedralSurface, QgsWkbTypes) +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestQgsPolyhedralSurface(QgisTestCase): + + def test_constructor(self): + surface = QgsPolyhedralSurface() + self.assertTrue(surface.isEmpty()) + self.assertEqual(surface.numPatches(), 0) + self.assertFalse(surface.is3D()) + self.assertFalse(surface.isMeasure()) + self.assertEqual(surface.wkbType(), QgsWkbTypes.Type.PolyhedralSurface) + self.assertEqual(surface.wktTypeStr(), "PolyhedralSurface") + self.assertEqual(surface.geometryType(), "PolyhedralSurface") + self.assertEqual(surface.dimension(), 2) + + def test_wkt(self): + # 2D + surface = QgsPolyhedralSurface() + surface.fromWkt('POLYHEDRALSURFACE (((0 0,0 1,1 1,1 0,0 0)))') + self.assertFalse(surface.isEmpty()) + self.assertEqual(surface.numPatches(), 1) + self.assertFalse(surface.is3D()) + self.assertFalse(surface.isMeasure()) + self.assertEqual(surface.wkbType(), QgsWkbTypes.Type.PolyhedralSurface) + + surface2 = QgsPolyhedralSurface() + surface2.fromWkt(surface.asWkt()) + self.assertEqual(surface, surface2) + + # 3D + surfaceZ = QgsPolyhedralSurface() + surfaceZ.fromWkt('POLYHEDRALSURFACE Z (((0 0 0,0 1 0,1 1 0,0 0 0)))') + self.assertFalse(surfaceZ.isEmpty()) + self.assertEqual(surfaceZ.numPatches(), 1) + self.assertTrue(surfaceZ.is3D()) + self.assertFalse(surfaceZ.isMeasure()) + self.assertEqual(surfaceZ.wkbType(), QgsWkbTypes.Type.PolyhedralSurfaceZ) + + surfaceZ2 = QgsPolyhedralSurface() + surfaceZ2.fromWkt(surfaceZ.asWkt()) + self.assertEqual(surfaceZ, surfaceZ2) + + # Measure + surfaceM = QgsPolyhedralSurface() + surfaceM.fromWkt('POLYHEDRALSURFACE M (((0 0 3,0 1 3,1 1 3,0 0 3)))') + self.assertFalse(surfaceM.isEmpty()) + self.assertEqual(surfaceM.numPatches(), 1) + self.assertFalse(surfaceM.is3D()) + self.assertTrue(surfaceM.isMeasure()) + self.assertEqual(surfaceM.wkbType(), QgsWkbTypes.Type.PolyhedralSurfaceM) + + surfaceM2 = QgsPolyhedralSurface() + surfaceM2.fromWkt(surfaceM.asWkt()) + self.assertEqual(surfaceM, surfaceM2) + + # ZM + surfaceZM = QgsPolyhedralSurface() + surfaceZM.fromWkt('POLYHEDRALSURFACE ZM ' + '(((0 0 1 2,0 1 1 2,1 1 1 2,0 0 1 2)),' + '((10 10 0 0,10 11 0 0,11 11 0 0,10 10 0 0)))') + self.assertFalse(surfaceZM.isEmpty()) + self.assertEqual(surfaceZM.numPatches(), 2) + self.assertTrue(surfaceZM.is3D()) + self.assertTrue(surfaceZM.isMeasure()) + self.assertEqual(surfaceZM.wkbType(), QgsWkbTypes.Type.PolyhedralSurfaceZM) + + surfaceZM2 = QgsPolyhedralSurface() + surfaceZM2.fromWkt(surfaceZM.asWkt()) + self.assertEqual(surfaceZM, surfaceZM2) + + def test_patch(self): + surface = QgsPolyhedralSurface() + self.assertTrue(surface.isEmpty()) + self.assertEqual(surface.numPatches(), 0) + self.assertFalse(surface.is3D()) + self.assertFalse(surface.isMeasure()) + + patch1 = QgsPolygon() + patchExterior1 = QgsLineString( + [QgsPoint(0, 0), QgsPoint(0, 10), QgsPoint(10, 10), QgsPoint(10, 0), + QgsPoint(0, 0)]) + patch1.setExteriorRing(patchExterior1) + patchInteriorRing = QgsLineString( + [QgsPoint(1, 1), QgsPoint(1, 9), QgsPoint(9, 9), QgsPoint(9, 1), + QgsPoint(1, 1)]) + patch1.addInteriorRing(patchInteriorRing) + surface.addPatch(patch1) + self.assertEqual(surface.numPatches(), 1) + self.assertFalse(surface.is3D()) + self.assertFalse(surface.isMeasure()) + + patch2 = QgsPolygon() + patchExterior2 = QgsLineString( + [QgsPoint(10, 0), QgsPoint(10, 10), QgsPoint(20, 10), QgsPoint(20, 0), + QgsPoint(10, 0)]) + patch2.setExteriorRing(patchExterior2) + surface.addPatch(patch2) + self.assertEqual(surface.numPatches(), 2) + self.assertFalse(surface.is3D()) + self.assertFalse(surface.isMeasure()) + + surface.clear() + self.assertEqual(surface.numPatches(), 0) + self.assertFalse(surface.is3D()) + self.assertFalse(surface.isMeasure()) + + def test_len(self): + surface1 = QgsPolyhedralSurface() + self.assertEqual(surface1.numPatches(), 0) + self.assertEqual(len(surface1), 0) + + patch1 = QgsPolygon() + patchExterior1 = QgsLineString( + [QgsPoint(0, 0), QgsPoint(0, 10), QgsPoint(10, 10), QgsPoint(10, 0), + QgsPoint(0, 0)]) + patch1.setExteriorRing(patchExterior1) + patchInteriorRing = QgsLineString( + [QgsPoint(1, 1), QgsPoint(1, 9), QgsPoint(9, 9), QgsPoint(9, 1), + QgsPoint(1, 1)]) + patch1.addInteriorRing(patchInteriorRing) + surface1.addPatch(patch1) + self.assertEqual(surface1.numPatches(), 1) + self.assertEqual(len(surface1), 1) + + surface2 = QgsPolyhedralSurface() + surface2.fromWkt('POLYHEDRALSURFACE ZM ' + '(((0 0 1 2,0 1 1 2,1 1 1 2,0 0 1 2)),' + '((10 10 0 0,10 11 0 0,11 11 0 0,10 10 0 0)))') + self.assertTrue(surface2.numPatches(), 2) + self.assertTrue(len(surface2), 2) + + def test_getitem(self): + surface = QgsPolyhedralSurface() + with self.assertRaises(IndexError): + surface[0] + with self.assertRaises(IndexError): + surface[-1] + + patch1 = QgsPolygon() + patchExterior1 = QgsLineString( + [QgsPoint(0, 0), QgsPoint(0, 10), QgsPoint(10, 10), QgsPoint(10, 0), + QgsPoint(0, 0)]) + patch1.setExteriorRing(patchExterior1) + patchInteriorRing = QgsLineString( + [QgsPoint(1, 1), QgsPoint(1, 9), QgsPoint(9, 9), QgsPoint(9, 1), + QgsPoint(1, 1)]) + patch1.addInteriorRing(patchInteriorRing) + surface.addPatch(patch1) + self.assertEqual(surface.numPatches(), 1) + self.assertEqual(surface[0], patch1) + self.assertEqual(surface[-1], patch1) + with self.assertRaises(IndexError): + surface[1] + with self.assertRaises(IndexError): + surface[-2] + + patch2 = QgsPolygon() + patchExterior2 = QgsLineString( + [QgsPoint(10, 0), QgsPoint(10, 10), QgsPoint(20, 10), QgsPoint(20, 0), + QgsPoint(10, 0)]) + patch2.setExteriorRing(patchExterior2) + surface.addPatch(patch2) + self.assertEqual(surface.numPatches(), 2) + self.assertEqual(surface[0], patch1) + self.assertEqual(surface[1], patch2) + self.assertEqual(surface[-1], patch2) + self.assertEqual(surface[-2], patch1) + with self.assertRaises(IndexError): + surface[2] + with self.assertRaises(IndexError): + surface[-3] + + +if __name__ == '__main__': + unittest.main()