diff --git a/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in index 46dd402fa9ec..c6d3333e10b2 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in @@ -604,7 +604,7 @@ E.g. :py:class:`QgsLineString` -> :py:class:`QgsCompoundCurve`, :py:class:`QgsPo :return: the converted geometry. Caller takes ownership %End - virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const = 0 /Factory/; + virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const = 0 /Factory/; %Docstring Makes a new geometry with all the points or vertices snapped to the closest point of the grid. Ownership is transferred to the caller. @@ -626,6 +626,7 @@ In this case, it can be thought like rounding the x and y of all the points/vert :param vSpacing: Vertical spacing of the grid (y axis). 0 to disable. :param dSpacing: Depth spacing of the grid (z axis). 0 (default) to disable. :param mSpacing: Custom dimension spacing of the grid (m axis). 0 (default) to disable. +:param removeRedundantPoints: if ``True``, then points which are redundant (e.g. they represent mid points on a straight line segment) will be skipped (since QGIS 3.38) %End virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) = 0; diff --git a/python/PyQt6/core/auto_generated/geometry/qgscircularstring.sip.in b/python/PyQt6/core/auto_generated/geometry/qgscircularstring.sip.in index df661bd14cea..45907d1c04e0 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgscircularstring.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgscircularstring.sip.in @@ -151,7 +151,7 @@ Appends the contents of another circular ``string`` to the end of this circular virtual QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/; - virtual QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/PyQt6/core/auto_generated/geometry/qgscompoundcurve.sip.in b/python/PyQt6/core/auto_generated/geometry/qgscompoundcurve.sip.in index bc7256ac6460..72926351545c 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgscompoundcurve.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgscompoundcurve.sip.in @@ -81,7 +81,7 @@ of the curve. :param toleranceType: maximum segmentation angle or maximum difference between approximation and curve %End - virtual QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/PyQt6/core/auto_generated/geometry/qgscurvepolygon.sip.in b/python/PyQt6/core/auto_generated/geometry/qgscurvepolygon.sip.in index 03c288a5c6c2..b5137c053304 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgscurvepolygon.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgscurvepolygon.sip.in @@ -68,7 +68,7 @@ Curve polygon geometry type virtual QgsAbstractGeometry *boundary() const /Factory/; - virtual QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometrycollection.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometrycollection.sip.in index 9f21b5c174dc..ca1c257a90ec 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgsgeometrycollection.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometrycollection.sip.in @@ -95,7 +95,7 @@ Returns a geometry from within the collection. virtual void clear(); - virtual QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in b/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in index 61ce1397087c..10c95a6cfcf5 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgslinestring.sip.in @@ -594,7 +594,7 @@ segment in the line. int indexOf( const QgsPoint &point ) const final; virtual bool isValid( QString &error /Out/, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const; - virtual QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/PyQt6/core/auto_generated/geometry/qgspoint.sip.in b/python/PyQt6/core/auto_generated/geometry/qgspoint.sip.in index c7fa57bd4b54..9e87b2448699 100644 --- a/python/PyQt6/core/auto_generated/geometry/qgspoint.sip.in +++ b/python/PyQt6/core/auto_generated/geometry/qgspoint.sip.in @@ -355,7 +355,7 @@ Example virtual QgsPoint *clone() const /Factory/; - virtual QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in b/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in index 53b200c9ba01..da295d423ad1 100644 --- a/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in @@ -604,7 +604,7 @@ E.g. :py:class:`QgsLineString` -> :py:class:`QgsCompoundCurve`, :py:class:`QgsPo :return: the converted geometry. Caller takes ownership %End - virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const = 0 /Factory/; + virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const = 0 /Factory/; %Docstring Makes a new geometry with all the points or vertices snapped to the closest point of the grid. Ownership is transferred to the caller. @@ -626,6 +626,7 @@ In this case, it can be thought like rounding the x and y of all the points/vert :param vSpacing: Vertical spacing of the grid (y axis). 0 to disable. :param dSpacing: Depth spacing of the grid (z axis). 0 (default) to disable. :param mSpacing: Custom dimension spacing of the grid (m axis). 0 (default) to disable. +:param removeRedundantPoints: if ``True``, then points which are redundant (e.g. they represent mid points on a straight line segment) will be skipped (since QGIS 3.38) %End virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ) = 0; diff --git a/python/core/auto_generated/geometry/qgscircularstring.sip.in b/python/core/auto_generated/geometry/qgscircularstring.sip.in index df661bd14cea..45907d1c04e0 100644 --- a/python/core/auto_generated/geometry/qgscircularstring.sip.in +++ b/python/core/auto_generated/geometry/qgscircularstring.sip.in @@ -151,7 +151,7 @@ Appends the contents of another circular ``string`` to the end of this circular virtual QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/; - virtual QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/core/auto_generated/geometry/qgscompoundcurve.sip.in b/python/core/auto_generated/geometry/qgscompoundcurve.sip.in index bc7256ac6460..72926351545c 100644 --- a/python/core/auto_generated/geometry/qgscompoundcurve.sip.in +++ b/python/core/auto_generated/geometry/qgscompoundcurve.sip.in @@ -81,7 +81,7 @@ of the curve. :param toleranceType: maximum segmentation angle or maximum difference between approximation and curve %End - virtual QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/core/auto_generated/geometry/qgscurvepolygon.sip.in b/python/core/auto_generated/geometry/qgscurvepolygon.sip.in index 03c288a5c6c2..b5137c053304 100644 --- a/python/core/auto_generated/geometry/qgscurvepolygon.sip.in +++ b/python/core/auto_generated/geometry/qgscurvepolygon.sip.in @@ -68,7 +68,7 @@ Curve polygon geometry type virtual QgsAbstractGeometry *boundary() const /Factory/; - virtual QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/core/auto_generated/geometry/qgsgeometrycollection.sip.in b/python/core/auto_generated/geometry/qgsgeometrycollection.sip.in index 11e70dcbc45b..d05ede7b98b8 100644 --- a/python/core/auto_generated/geometry/qgsgeometrycollection.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometrycollection.sip.in @@ -95,7 +95,7 @@ Returns a geometry from within the collection. virtual void clear(); - virtual QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/core/auto_generated/geometry/qgslinestring.sip.in b/python/core/auto_generated/geometry/qgslinestring.sip.in index 61ce1397087c..10c95a6cfcf5 100644 --- a/python/core/auto_generated/geometry/qgslinestring.sip.in +++ b/python/core/auto_generated/geometry/qgslinestring.sip.in @@ -594,7 +594,7 @@ segment in the line. int indexOf( const QgsPoint &point ) const final; virtual bool isValid( QString &error /Out/, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const; - virtual QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/python/core/auto_generated/geometry/qgspoint.sip.in b/python/core/auto_generated/geometry/qgspoint.sip.in index c7fa57bd4b54..9e87b2448699 100644 --- a/python/core/auto_generated/geometry/qgspoint.sip.in +++ b/python/core/auto_generated/geometry/qgspoint.sip.in @@ -355,7 +355,7 @@ Example virtual QgsPoint *clone() const /Factory/; - virtual QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const /Factory/; + virtual QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/; virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false ); diff --git a/src/app/qgsattributetabledialog.cpp b/src/app/qgsattributetabledialog.cpp index c2bca62e6934..71be5cac9922 100644 --- a/src/app/qgsattributetabledialog.cpp +++ b/src/app/qgsattributetabledialog.cpp @@ -217,16 +217,18 @@ QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *layer, QgsAttr { request.setFilterExpression( filterExpression ); } + + // If sort expression requires geometry, we'll need to fetch it + needsGeom |= mLayer && QgsExpression( mLayer->attributeTableConfig().sortExpression() ).needsGeometry(); if ( !needsGeom ) request.setFlags( Qgis::FeatureRequestFlag::NoGeometry ); - // Initialize dual view if ( mLayer ) { request.setSubsetOfAttributes( mMainView->requiredAttributes( mLayer ) ); mMainView->init( mLayer, QgisApp::instance()->mapCanvas(), request, editorContext, false ); - QgsAttributeTableConfig config = mLayer->attributeTableConfig(); + const QgsAttributeTableConfig config = mLayer->attributeTableConfig(); mMainView->setAttributeTableConfig( config ); mFeatureFilterWidget->init( mLayer, editorContext, mMainView, QgisApp::instance()->messageBar(), QgsMessageBar::defaultMessageTimeout() ); } diff --git a/src/core/geometry/qgsabstractgeometry.h b/src/core/geometry/qgsabstractgeometry.h index 4f1e0d7fb323..4a2c382759c4 100644 --- a/src/core/geometry/qgsabstractgeometry.h +++ b/src/core/geometry/qgsabstractgeometry.h @@ -644,8 +644,9 @@ class CORE_EXPORT QgsAbstractGeometry * \param vSpacing Vertical spacing of the grid (y axis). 0 to disable. * \param dSpacing Depth spacing of the grid (z axis). 0 (default) to disable. * \param mSpacing Custom dimension spacing of the grid (m axis). 0 (default) to disable. + * \param removeRedundantPoints if TRUE, then points which are redundant (e.g. they represent mid points on a straight line segment) will be skipped (since QGIS 3.38) */ - virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const = 0 SIP_FACTORY; + virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const = 0 SIP_FACTORY; /** * Removes duplicate nodes from the geometry, wherever removing the nodes does not result in a diff --git a/src/core/geometry/qgscircularstring.cpp b/src/core/geometry/qgscircularstring.cpp index 22a6ed99a8bb..c9a0f07c565e 100644 --- a/src/core/geometry/qgscircularstring.cpp +++ b/src/core/geometry/qgscircularstring.cpp @@ -624,13 +624,14 @@ QgsLineString *QgsCircularString::curveToLine( double tolerance, SegmentationTol return line; } -QgsCircularString *QgsCircularString::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing ) const +QgsCircularString *QgsCircularString::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool ) const { // prepare result std::unique_ptr result { createEmptyWithSameType() }; + // remove redundant not supported for circular strings bool res = snapToGridPrivate( hSpacing, vSpacing, dSpacing, mSpacing, mX, mY, mZ, mM, - result->mX, result->mY, result->mZ, result->mM ); + result->mX, result->mY, result->mZ, result->mM, false ); if ( res ) return result.release(); else diff --git a/src/core/geometry/qgscircularstring.h b/src/core/geometry/qgscircularstring.h index 81fa3aa2a3c8..9b2dae93dcc1 100644 --- a/src/core/geometry/qgscircularstring.h +++ b/src/core/geometry/qgscircularstring.h @@ -259,7 +259,7 @@ class CORE_EXPORT QgsCircularString: public QgsCurve QgsPoint startPoint() const override SIP_HOLDGIL; QgsPoint endPoint() const override SIP_HOLDGIL; QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const override SIP_FACTORY; - QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsCircularString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const override SIP_FACTORY; bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits::epsilon(), bool useZValues = false ) override; void draw( QPainter &p ) const override; diff --git a/src/core/geometry/qgscompoundcurve.cpp b/src/core/geometry/qgscompoundcurve.cpp index f66646bba8a3..af793f5af26b 100644 --- a/src/core/geometry/qgscompoundcurve.cpp +++ b/src/core/geometry/qgscompoundcurve.cpp @@ -465,13 +465,13 @@ QgsLineString *QgsCompoundCurve::curveToLine( double tolerance, SegmentationTole return line; } -QgsCompoundCurve *QgsCompoundCurve::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing ) const +QgsCompoundCurve *QgsCompoundCurve::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const { std::unique_ptr result( createEmptyWithSameType() ); for ( QgsCurve *curve : mCurves ) { - std::unique_ptr gridified( static_cast< QgsCurve * >( curve->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing ) ) ); + std::unique_ptr gridified( static_cast< QgsCurve * >( curve->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing, removeRedundantPoints ) ) ); if ( gridified ) { result->mCurves.append( gridified.release() ); @@ -825,6 +825,7 @@ bool QgsCompoundCurve::deleteVertex( QgsVertexId position ) removeCurve( curveId ); } } + // We are on a vertex that belongs to two curves else if ( curveIds.size() == 2 ) { const int nextCurveId = curveIds.at( 1 ).first; @@ -835,46 +836,51 @@ bool QgsCompoundCurve::deleteVertex( QgsVertexId position ) Q_ASSERT( subVertexId.vertex == curve->numPoints() - 1 ); Q_ASSERT( nextSubVertexId.vertex == 0 ); + // globals start and end points const QgsPoint startPoint = curve->startPoint(); const QgsPoint endPoint = nextCurve->endPoint(); - if ( QgsWkbTypes::flatType( curve->wkbType() ) == Qgis::WkbType::LineString && - QgsWkbTypes::flatType( nextCurve->wkbType() ) == Qgis::WkbType::CircularString && - nextCurve->numPoints() > 3 ) - { - QgsPoint intermediatePoint; - Qgis::VertexType type; - nextCurve->pointAt( 2, intermediatePoint, type ); - curve->moveVertex( QgsVertexId( 0, 0, curve->numPoints() - 1 ), intermediatePoint ); - } - else if ( !curve->deleteVertex( subVertexId ) ) + // delete the vertex on first curve + if ( !curve->deleteVertex( subVertexId ) ) { clearCache(); //bbox may have changed return false; } - if ( QgsWkbTypes::flatType( curve->wkbType() ) == Qgis::WkbType::CircularString && - curve->numPoints() > 0 && - QgsWkbTypes::flatType( nextCurve->wkbType() ) == Qgis::WkbType::LineString ) - { - QgsPoint intermediatePoint = curve->endPoint(); - nextCurve->moveVertex( QgsVertexId( 0, 0, 0 ), intermediatePoint ); - } - else if ( !nextCurve->deleteVertex( nextSubVertexId ) ) + + // delete the vertex on second curve + if ( !nextCurve->deleteVertex( nextSubVertexId ) ) { clearCache(); //bbox may have changed return false; } - if ( curve->numPoints() == 0 && - nextCurve->numPoints() != 0 ) + + // if first curve is now empty and second is not then + // create a LineString to link from the global start point to the + // new start of the second curve and delete the first curve + if ( curve->numPoints() == 0 && nextCurve->numPoints() != 0 ) { - nextCurve->moveVertex( QgsVertexId( 0, 0, 0 ), startPoint ); + QgsPoint startPointOfSecond = nextCurve->startPoint(); removeCurve( curveId ); + QgsLineString *line = new QgsLineString(); + line->insertVertex( QgsVertexId( 0, 0, 0 ), startPoint ); + line->insertVertex( QgsVertexId( 0, 0, 1 ), startPointOfSecond ); + mCurves.insert( curveId, line ); } + // else, if the first curve is not empty and the second is + // then create a LineString to link from the new end of the first curve to the + // global end point and delete the first curve else if ( curve->numPoints() != 0 && nextCurve->numPoints() == 0 ) { - curve->moveVertex( QgsVertexId( 0, 0, curve->numPoints() - 1 ), endPoint ); + QgsPoint endPointOfFirst = curve->endPoint(); removeCurve( nextCurveId ); + QgsLineString *line = new QgsLineString(); + line->insertVertex( QgsVertexId( 0, 0, 0 ), endPointOfFirst ); + line->insertVertex( QgsVertexId( 0, 0, 1 ), endPoint ); + mCurves.insert( nextCurveId, line ); } + // else, if both curves are empty then + // remove both curves and create a LineString to link + // the curves before and the curves after the whole geometry else if ( curve->numPoints() == 0 && nextCurve->numPoints() == 0 ) { @@ -885,6 +891,8 @@ bool QgsCompoundCurve::deleteVertex( QgsVertexId position ) line->insertVertex( QgsVertexId( 0, 0, 1 ), endPoint ); mCurves.insert( curveId, line ); } + // else, both curves still have vertices, create a LineString to link + // the curves if needed else { QgsPoint endPointOfFirst = curve->endPoint(); @@ -897,6 +905,7 @@ bool QgsCompoundCurve::deleteVertex( QgsVertexId position ) mCurves.insert( nextCurveId, line ); } } + condenseCurves(); // We merge consecutive LineStrings and CircularStrings } bool success = !curveIds.isEmpty(); diff --git a/src/core/geometry/qgscompoundcurve.h b/src/core/geometry/qgscompoundcurve.h index 8fc1c5e76807..6793e2c5fa23 100644 --- a/src/core/geometry/qgscompoundcurve.h +++ b/src/core/geometry/qgscompoundcurve.h @@ -121,7 +121,7 @@ class CORE_EXPORT QgsCompoundCurve: public QgsCurve */ QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const override SIP_FACTORY; - QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsCompoundCurve *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) 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; const QgsAbstractGeometry *simplifiedTypeRef() const override SIP_HOLDGIL; diff --git a/src/core/geometry/qgscurve.cpp b/src/core/geometry/qgscurve.cpp index 64c7a2b74e4d..e256f914993c 100644 --- a/src/core/geometry/qgscurve.cpp +++ b/src/core/geometry/qgscurve.cpp @@ -316,91 +316,121 @@ QgsPoint QgsCurve::childPoint( int index ) const bool QgsCurve::snapToGridPrivate( double hSpacing, double vSpacing, double dSpacing, double mSpacing, const QVector &srcX, const QVector &srcY, const QVector &srcZ, const QVector &srcM, - QVector &outX, QVector &outY, QVector &outZ, QVector &outM ) const + QVector &outX, QVector &outY, QVector &outZ, QVector &outM, bool removeRedundantPoints ) const { const int length = numPoints(); - - if ( length <= 0 ) + if ( length < 2 ) return false; const bool hasZ = is3D(); const bool hasM = isMeasure(); - // helper functions - auto roundVertex = [hSpacing, vSpacing, dSpacing, mSpacing, hasZ, hasM, &srcX, &srcY, &srcZ, &srcM]( QgsPoint & out, int i ) + outX.reserve( length ); + outY.reserve( length ); + if ( hasZ ) + outZ.reserve( length ); + if ( hasM ) + outM.reserve( length ); + + const double *xIn = srcX.constData(); + const double *yIn = srcY.constData(); + const double *zIn = hasZ ? srcZ.constData() : nullptr; + const double *mIn = hasM ? srcM.constData() : nullptr; + + double previousX = 0; + double previousY = 0; + double previousZ = 0; + double previousM = 0; + int outSize = 0; + for ( int i = 0; i < length; ++i ) { - if ( hSpacing > 0 ) - out.setX( std::round( srcX.at( i ) / hSpacing ) * hSpacing ); - else - out.setX( srcX.at( i ) ); + const double currentX = *xIn++; + const double currentY = *yIn++; + const double currentZ = zIn ? *zIn++ : 0; + const double currentM = mIn ? *mIn++ : 0; - if ( vSpacing > 0 ) - out.setY( std::round( srcY.at( i ) / vSpacing ) * vSpacing ); - else - out.setY( srcY.at( i ) ); + const double roundedX = hSpacing > 0 ? ( std::round( currentX / hSpacing ) * hSpacing ) : currentX; + const double roundedY = vSpacing > 0 ? ( std::round( currentY / vSpacing ) * vSpacing ) : currentY; + const double roundedZ = hasZ && dSpacing > 0 ? ( std::round( currentZ / dSpacing ) * dSpacing ) : currentZ; + const double roundedM = hasM && mSpacing > 0 ? ( std::round( currentM / mSpacing ) * mSpacing ) : currentM; - if ( hasZ ) + if ( i == 0 ) { - if ( dSpacing > 0 ) - out.setZ( std::round( srcZ.at( i ) / dSpacing ) * dSpacing ); - else - out.setZ( srcZ.at( i ) ); + outX.append( roundedX ); + outY.append( roundedY ); + if ( hasZ ) + outZ.append( roundedZ ); + if ( hasM ) + outM.append( roundedM ); + outSize++; } - - if ( hasM ) + else { - if ( mSpacing > 0 ) - out.setM( std::round( srcM.at( i ) / mSpacing ) * mSpacing ); + const bool isPointEqual = qgsDoubleNear( roundedX, previousX ) + && qgsDoubleNear( roundedY, previousY ) + && ( !hasZ || dSpacing <= 0 || qgsDoubleNear( roundedZ, previousZ ) ) + && ( !hasM || mSpacing <= 0 || qgsDoubleNear( roundedM, previousM ) ); + if ( isPointEqual ) + continue; + + // maybe previous point is redundant and is just a midpoint on a straight line -- let's check + bool previousPointRedundant = false; + if ( removeRedundantPoints && outSize > 1 && !hasZ && !hasM ) + { + previousPointRedundant = QgsGeometryUtilsBase::leftOfLine( outX.at( outSize - 1 ), + outY.at( outSize - 1 ), + outX.at( outSize - 2 ), + outY.at( outSize - 2 ), + roundedX, roundedY ) == 0; + } + if ( previousPointRedundant ) + { + outX[ outSize - 1 ] = roundedX; + outY[ outSize - 1 ] = roundedY; + } else - out.setM( srcM.at( i ) ); + { + outX.append( roundedX ); + outY.append( roundedY ); + if ( hasZ ) + outZ.append( roundedZ ); + if ( hasM ) + outM.append( roundedM ); + outSize++; + } } - }; - - auto append = [hasZ, hasM, &outX, &outY, &outM, &outZ]( QgsPoint const & point ) - { - outX.append( point.x() ); - - outY.append( point.y() ); - - if ( hasZ ) - outZ.append( point.z() ); - - if ( hasM ) - outM.append( point.m() ); - }; + previousX = roundedX; + previousY = roundedY; + previousZ = roundedZ; + previousM = roundedM; + } - auto isPointEqual = [dSpacing, mSpacing, hasZ, hasM]( const QgsPoint & a, const QgsPoint & b ) - { - return ( a.x() == b.x() ) - && ( a.y() == b.y() ) - && ( !hasZ || dSpacing <= 0 || a.z() == b.z() ) - && ( !hasM || mSpacing <= 0 || a.m() == b.m() ); - }; - - // temporary values - const Qgis::WkbType pointType = QgsWkbTypes::zmType( Qgis::WkbType::Point, hasZ, hasM ); - QgsPoint last( pointType ); - QgsPoint current( pointType ); - - // Actual code (what does all the work) - roundVertex( last, 0 ); - append( last ); - - for ( int i = 1; i < length; ++i ) + if ( removeRedundantPoints && isClosed() && outSize > 4 && !hasZ && !hasM ) { - roundVertex( current, i ); - if ( !isPointEqual( current, last ) ) + // maybe first/last vertex is redundant, let's try to remove that too + const bool firstVertexIsRedundant = QgsGeometryUtilsBase::leftOfLine( outX.at( 0 ), + outY.at( 0 ), + outX.at( outSize - 2 ), + outY.at( outSize - 2 ), + outX.at( 1 ), outY.at( 1 ) ) == 0; + if ( firstVertexIsRedundant ) { - append( current ); - last = current; + outX.removeAt( 0 ); + outY.removeAt( 0 ); + outX[ outSize - 2 ] = outX.at( 0 ); + outY[ outSize - 2 ] = outY.at( 0 ); } } - // if it's not closed, with 2 points you get a correct line - // if it is, you need at least 4 (3 + the vertex that closes) - if ( outX.length() < 2 || ( isClosed() && outX.length() < 4 ) ) - return false; + // we previously reserved size based on a worst case scenario, let's free + // unnecessary memory reservation now + outX.squeeze(); + outY.squeeze(); + if ( hasZ ) + outZ.squeeze(); + if ( hasM ) + outM.squeeze(); - return true; + return outSize >= 4 || ( !isClosed() && outSize >= 2 ); } diff --git a/src/core/geometry/qgscurve.h b/src/core/geometry/qgscurve.h index e21b8c928ff3..a02587bceca2 100644 --- a/src/core/geometry/qgscurve.h +++ b/src/core/geometry/qgscurve.h @@ -345,7 +345,8 @@ class CORE_EXPORT QgsCurve: public QgsAbstractGeometry SIP_ABSTRACT */ bool snapToGridPrivate( double hSpacing, double vSpacing, double dSpacing, double mSpacing, const QVector &srcX, const QVector &srcY, const QVector &srcZ, const QVector &srcM, - QVector &outX, QVector &outY, QVector &outZ, QVector &outM ) const; + QVector &outX, QVector &outY, QVector &outZ, QVector &outM, + bool removeRedundantPoints ) const; #endif /** diff --git a/src/core/geometry/qgscurvepolygon.cpp b/src/core/geometry/qgscurvepolygon.cpp index d68fe2f387c0..f12974806ea8 100644 --- a/src/core/geometry/qgscurvepolygon.cpp +++ b/src/core/geometry/qgscurvepolygon.cpp @@ -551,7 +551,7 @@ QgsAbstractGeometry *QgsCurvePolygon::boundary() const } } -QgsCurvePolygon *QgsCurvePolygon::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing ) const +QgsCurvePolygon *QgsCurvePolygon::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const { if ( !mExteriorRing ) return nullptr; @@ -560,7 +560,7 @@ QgsCurvePolygon *QgsCurvePolygon::snappedToGrid( double hSpacing, double vSpacin std::unique_ptr< QgsCurvePolygon > polygon( createEmptyWithSameType() ); // exterior ring - auto exterior = std::unique_ptr { static_cast< QgsCurve *>( mExteriorRing->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing ) ) }; + auto exterior = std::unique_ptr { static_cast< QgsCurve *>( mExteriorRing->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing, removeRedundantPoints ) ) }; if ( !exterior ) return nullptr; @@ -573,7 +573,7 @@ QgsCurvePolygon *QgsCurvePolygon::snappedToGrid( double hSpacing, double vSpacin if ( !interior ) continue; - QgsCurve *gridifiedInterior = static_cast< QgsCurve * >( interior->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing ) ); + QgsCurve *gridifiedInterior = static_cast< QgsCurve * >( interior->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing, removeRedundantPoints ) ); if ( !gridifiedInterior ) continue; diff --git a/src/core/geometry/qgscurvepolygon.h b/src/core/geometry/qgscurvepolygon.h index e609b3339e23..7bb7dd6d3230 100644 --- a/src/core/geometry/qgscurvepolygon.h +++ b/src/core/geometry/qgscurvepolygon.h @@ -137,7 +137,7 @@ class CORE_EXPORT QgsCurvePolygon: public QgsSurface double perimeter() const override SIP_HOLDGIL; QgsPolygon *surfaceToPolygon() const override SIP_FACTORY; QgsAbstractGeometry *boundary() const override SIP_FACTORY; - QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsCurvePolygon *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) 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; diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index 869bb55d8441..219170facf6c 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -947,7 +947,9 @@ Qgis::GeometryOperationResult QgsGeometry::addPart( const QVector &p { QgsPointSequence l; convertPointList( points, l ); + Q_NOWARN_DEPRECATED_PUSH return addPart( l, geomType ); + Q_NOWARN_DEPRECATED_POP } Qgis::GeometryOperationResult QgsGeometry::addPartV2( const QVector &points, Qgis::WkbType wkbType ) @@ -970,7 +972,9 @@ Qgis::GeometryOperationResult QgsGeometry::addPart( const QgsPointSequence &poin ringLine->setPoints( points ); partGeom = std::move( ringLine ); } + Q_NOWARN_DEPRECATED_PUSH return addPart( partGeom.release(), geomType ); + Q_NOWARN_DEPRECATED_POP } Qgis::GeometryOperationResult QgsGeometry::addPartV2( const QgsPointSequence &points, Qgis::WkbType wkbType ) diff --git a/src/core/geometry/qgsgeometrycollection.cpp b/src/core/geometry/qgsgeometrycollection.cpp index 058db833ba51..20cf15ed7338 100644 --- a/src/core/geometry/qgsgeometrycollection.cpp +++ b/src/core/geometry/qgsgeometrycollection.cpp @@ -92,13 +92,13 @@ void QgsGeometryCollection::clear() clearCache(); //set bounding box invalid } -QgsGeometryCollection *QgsGeometryCollection::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing ) const +QgsGeometryCollection *QgsGeometryCollection::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const { std::unique_ptr result; for ( auto geom : mGeometries ) { - std::unique_ptr gridified { geom->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing ) }; + std::unique_ptr gridified { geom->snappedToGrid( hSpacing, vSpacing, dSpacing, mSpacing, removeRedundantPoints ) }; if ( gridified ) { if ( !result ) diff --git a/src/core/geometry/qgsgeometrycollection.h b/src/core/geometry/qgsgeometrycollection.h index e83e04a79e83..4e5e009e9bfe 100644 --- a/src/core/geometry/qgsgeometrycollection.h +++ b/src/core/geometry/qgsgeometrycollection.h @@ -184,7 +184,7 @@ class CORE_EXPORT QgsGeometryCollection: public QgsAbstractGeometry int dimension() const override SIP_HOLDGIL; QString geometryType() const override SIP_HOLDGIL; void clear() override; - QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsGeometryCollection *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const override SIP_FACTORY; bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits::epsilon(), bool useZValues = false ) override; QgsAbstractGeometry *boundary() const override SIP_FACTORY; void adjacentVertices( QgsVertexId vertex, QgsVertexId &previousVertex SIP_OUT, QgsVertexId &nextVertex SIP_OUT ) const override; diff --git a/src/core/geometry/qgslinestring.cpp b/src/core/geometry/qgslinestring.cpp index 64e8a3c69d5b..d5f326bcf566 100644 --- a/src/core/geometry/qgslinestring.cpp +++ b/src/core/geometry/qgslinestring.cpp @@ -326,13 +326,13 @@ bool QgsLineString::isValid( QString &error, Qgis::GeometryValidityFlags flags ) return QgsCurve::isValid( error, flags ); } -QgsLineString *QgsLineString::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing ) const +QgsLineString *QgsLineString::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const { // prepare result std::unique_ptr result { createEmptyWithSameType() }; bool res = snapToGridPrivate( hSpacing, vSpacing, dSpacing, mSpacing, mX, mY, mZ, mM, - result->mX, result->mY, result->mZ, result->mM ); + result->mX, result->mY, result->mZ, result->mM, removeRedundantPoints ); if ( res ) return result.release(); else diff --git a/src/core/geometry/qgslinestring.h b/src/core/geometry/qgslinestring.h index 0079a33a3f6d..0c32ee61dda1 100644 --- a/src/core/geometry/qgslinestring.h +++ b/src/core/geometry/qgslinestring.h @@ -954,7 +954,7 @@ class CORE_EXPORT QgsLineString: public QgsCurve bool isEmpty() const override SIP_HOLDGIL; int indexOf( const QgsPoint &point ) const final; bool isValid( QString &error SIP_OUT, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const override; - QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsLineString *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const override SIP_FACTORY; bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits::epsilon(), bool useZValues = false ) override; bool isClosed() const override SIP_HOLDGIL; bool isClosed2D() const override SIP_HOLDGIL; diff --git a/src/core/geometry/qgspoint.cpp b/src/core/geometry/qgspoint.cpp index 9b6cebf23312..cf0090577db4 100644 --- a/src/core/geometry/qgspoint.cpp +++ b/src/core/geometry/qgspoint.cpp @@ -107,7 +107,7 @@ QgsPoint *QgsPoint::clone() const return new QgsPoint( *this ); } -QgsPoint *QgsPoint::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing ) const +QgsPoint *QgsPoint::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool ) const { // helper function auto gridifyValue = []( double value, double spacing, bool extraCondition = true ) -> double diff --git a/src/core/geometry/qgspoint.h b/src/core/geometry/qgspoint.h index 67523cad2de0..149e5e39fcb4 100644 --- a/src/core/geometry/qgspoint.h +++ b/src/core/geometry/qgspoint.h @@ -562,7 +562,7 @@ class CORE_EXPORT QgsPoint: public QgsAbstractGeometry QString geometryType() const override SIP_HOLDGIL; int dimension() const override SIP_HOLDGIL; QgsPoint *clone() const override SIP_FACTORY; - QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0 ) const override SIP_FACTORY; + QgsPoint *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const override SIP_FACTORY; bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits::epsilon(), bool useZValues = false ) override; void clear() override; bool fromWkb( QgsConstWkbPtr &wkb ) override; diff --git a/src/core/labeling/qgspallabeling.cpp b/src/core/labeling/qgspallabeling.cpp index 4e945236f436..470bbdc718d3 100644 --- a/src/core/labeling/qgspallabeling.cpp +++ b/src/core/labeling/qgspallabeling.cpp @@ -1542,13 +1542,15 @@ bool QgsPalLayerSettings::checkMinimumSizeMM( const QgsRenderContext &ct, const return QgsPalLabeling::checkMinimumSizeMM( ct, geom, minSize ); } -void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f, QgsRenderContext *context, double *rotatedLabelX, double *rotatedLabelY, QgsTextDocument *document, QgsTextDocumentMetrics *documentMetrics, QRectF *outerBounds ) +void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f, QgsRenderContext *context, double *rotatedLabelX, double *rotatedLabelY, QgsTextFormat *specifiedFormat, QgsTextDocument *document, QgsTextDocumentMetrics *documentMetrics, QRectF *outerBounds ) { if ( !fm || !f ) { return; } + const QgsTextFormat *format = specifiedFormat ? specifiedFormat : &mFormat; + QString textCopy( text ); //try to keep < 2.12 API - handle no passed render context @@ -1563,8 +1565,8 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QSt QString wrapchr = wrapChar; int evalAutoWrapLength = autoWrapLength; - double multilineH = mFormat.lineHeight(); - Qgis::TextOrientation orientation = mFormat.orientation(); + double multilineH = format->lineHeight(); + Qgis::TextOrientation orientation = format->orientation(); bool addDirSymb = mLineSettings.addDirectionSymbol(); QString leftDirSymb = mLineSettings.leftDirectionSymbol(); @@ -1707,23 +1709,30 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QSt { document->splitLines( wrapchr, evalAutoWrapLength, useMaxLineLengthForAutoWrap ); - *documentMetrics = QgsTextDocumentMetrics::calculateMetrics( *document, mFormat, *rc ); - const QSizeF size = documentMetrics->documentSize( Qgis::TextLayoutMode::Labeling, orientation ); + *documentMetrics = QgsTextDocumentMetrics::calculateMetrics( *document, *format, *rc ); + const QSizeF size = documentMetrics->documentSize( Qgis::TextLayoutMode::Labeling, orientation != Qgis::TextOrientation::RotationBased ? orientation : Qgis::TextOrientation::Horizontal ); w = size.width(); h = size.height(); + + if ( orientation == Qgis::TextOrientation::RotationBased ) + { + const QSizeF rotatedSize = documentMetrics->documentSize( Qgis::TextLayoutMode::Labeling, Qgis::TextOrientation::Vertical ); + rh = rotatedSize.width(); + rw = rotatedSize.height(); + } } else { const QStringList multiLineSplit = QgsPalLabeling::splitToLines( textCopy, wrapchr, evalAutoWrapLength, useMaxLineLengthForAutoWrap ); const int lines = multiLineSplit.size(); - const double lineHeightPainterUnits = rc->convertToPainterUnits( mFormat.lineHeight(), mFormat.lineHeightUnit() ); + const double lineHeightPainterUnits = rc->convertToPainterUnits( format->lineHeight(), format->lineHeightUnit() ); switch ( orientation ) { case Qgis::TextOrientation::Horizontal: { - h += fm->height() + static_cast< double >( ( lines - 1 ) * ( mFormat.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelHeight * multilineH ) : lineHeightPainterUnits ) ); + h += fm->height() + static_cast< double >( ( lines - 1 ) * ( format->lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelHeight * multilineH ) : lineHeightPainterUnits ) ); for ( const QString &line : std::as_const( multiLineSplit ) ) { @@ -1734,9 +1743,9 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QSt case Qgis::TextOrientation::Vertical: { - double letterSpacing = mFormat.scaledFont( *context ).letterSpacing(); + double letterSpacing = format->scaledFont( *context ).letterSpacing(); double labelWidth = fm->maxWidth(); - w = labelWidth + ( lines - 1 ) * ( mFormat.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelWidth * multilineH ) : lineHeightPainterUnits ); + w = labelWidth + ( lines - 1 ) * ( format->lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelWidth * multilineH ) : lineHeightPainterUnits ); int maxLineLength = 0; for ( const QString &line : std::as_const( multiLineSplit ) ) @@ -1756,12 +1765,12 @@ void QgsPalLayerSettings::calculateLabelSize( const QFontMetricsF *fm, const QSt } double widthVertical = 0.0; - double letterSpacing = mFormat.scaledFont( *context ).letterSpacing(); + double letterSpacing = format->scaledFont( *context ).letterSpacing(); double labelWidth = fm->maxWidth(); - widthVertical = labelWidth + ( lines - 1 ) * ( mFormat.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelWidth * multilineH ) : lineHeightPainterUnits ); + widthVertical = labelWidth + ( lines - 1 ) * ( format->lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelWidth * multilineH ) : lineHeightPainterUnits ); double heightHorizontal = 0.0; - heightHorizontal += fm->height() + static_cast< double >( ( lines - 1 ) * ( mFormat.lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelHeight * multilineH ) : lineHeightPainterUnits ) ); + heightHorizontal += fm->height() + static_cast< double >( ( lines - 1 ) * ( format->lineHeightUnit() == Qgis::RenderUnit::Percentage ? ( labelHeight * multilineH ) : lineHeightPainterUnits ) ); double heightVertical = 0.0; int maxLineLength = 0; @@ -1911,11 +1920,13 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails } } - QFont labelFont = mFormat.font(); + QgsTextFormat evaluatedFormat = mFormat; + + QFont labelFont = evaluatedFormat.font(); // labelFont will be added to label feature for use during label painting // data defined font units? - Qgis::RenderUnit fontunits = mFormat.sizeUnit(); + Qgis::RenderUnit fontunits = evaluatedFormat.sizeUnit(); exprVal = mDataDefinedProperties.value( QgsPalLayerSettings::Property::FontSizeUnit, context.expressionContext() ); if ( !QgsVariantUtils::isNull( exprVal ) ) { @@ -1930,7 +1941,7 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails } //data defined label size? - double fontSize = mFormat.size(); + double fontSize = evaluatedFormat.size(); if ( mDataDefinedProperties.isActive( QgsPalLayerSettings::Property::Size ) ) { context.expressionContext().setOriginalValueVariable( fontSize ); @@ -1941,7 +1952,7 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails return nullptr; } - int fontPixelSize = QgsTextRenderer::sizeToPixel( fontSize, context, fontunits, mFormat.sizeMapUnitScale() ); + int fontPixelSize = QgsTextRenderer::sizeToPixel( fontSize, context, fontunits, evaluatedFormat.sizeMapUnitScale() ); // don't try to show font sizes less than 1 pixel (Qt complains) if ( fontPixelSize < 1 ) { @@ -1983,6 +1994,13 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails parseDropShadow( context ); } + evaluatedFormat.setFont( labelFont ); + // undo scaling by symbology reference scale, as this would have been applied in the previous call to QgsTextRenderer::sizeToPixel and we risk + // double-applying it if we don't re-adjust, since all the text format metric calculations assume an unscaled format font size is present + const double symbologyReferenceScaleFactor = context.symbologyReferenceScale() > 0 ? context.symbologyReferenceScale() / context.rendererScale() : 1; + evaluatedFormat.setSize( labelFont.pixelSize() / symbologyReferenceScaleFactor ); + evaluatedFormat.setSizeUnit( Qgis::RenderUnit::Pixels ); + QString labelText; // Check to see if we are a expression string. @@ -2016,7 +2034,7 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails } // apply capitalization - Qgis::Capitalization capitalization = mFormat.capitalization(); + Qgis::Capitalization capitalization = evaluatedFormat.capitalization(); // maintain API - capitalization may have been set in textFont if ( capitalization == Qgis::Capitalization::MixedCase && mFormat.font().capitalization() != QFont::MixedCase ) { @@ -2109,15 +2127,45 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails QgsTextDocument doc; QgsTextDocumentMetrics documentMetrics; QRectF outerBounds; - if ( format().allowHtmlFormatting() && !labelText.isEmpty() ) - { - doc = QgsTextDocument::fromHtml( QStringList() << labelText ); - // also applies the line split to doc and calculates document metrics! - calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, &doc, &documentMetrics, &outerBounds ); - } - else + + switch ( placement ) { - calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, nullptr, nullptr, &outerBounds ); + case Qgis::LabelPlacement::PerimeterCurved: + case Qgis::LabelPlacement::Curved: + { + // avoid calculating document and metrics if we don't require them for curved labels + if ( evaluatedFormat.allowHtmlFormatting() && !labelText.isEmpty() ) + { + doc = QgsTextDocument::fromHtml( QStringList() << labelText ); + calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, &evaluatedFormat, &doc, &documentMetrics, &outerBounds ); + } + else + { + calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, &evaluatedFormat, nullptr, nullptr, &outerBounds ); + } + break; + } + + case Qgis::LabelPlacement::AroundPoint: + case Qgis::LabelPlacement::OverPoint: + case Qgis::LabelPlacement::Line: + case Qgis::LabelPlacement::Horizontal: + case Qgis::LabelPlacement::Free: + case Qgis::LabelPlacement::OrderedPositionsAroundPoint: + case Qgis::LabelPlacement::OutsidePolygons: + { + // non-curved labels always require document and metrics + if ( evaluatedFormat.allowHtmlFormatting() && !labelText.isEmpty() ) + { + doc = QgsTextDocument::fromHtml( QStringList() << labelText ); + } + else + { + doc = QgsTextDocument::fromPlainText( { labelText } ); + } + calculateLabelSize( labelFontMetrics.get(), labelText, labelWidth, labelHeight, mCurFeat, &context, &rotatedLabelX, &rotatedLabelY, &evaluatedFormat, &doc, &documentMetrics, &outerBounds ); + break; + } } // maximum angle between curved label characters (hardcoded defaults used in QGIS <2.0) @@ -2818,7 +2866,7 @@ std::unique_ptr QgsPalLayerSettings::registerFeatureWithDetails case Qgis::LabelPlacement::Curved: case Qgis::LabelPlacement::PerimeterCurved: - labelFeature->setTextMetrics( QgsTextLabelFeature::calculateTextMetrics( xform, context, labelFont, *labelFontMetrics, labelFont.letterSpacing(), labelFont.wordSpacing(), labelText, format().allowHtmlFormatting() ? &doc : nullptr, format().allowHtmlFormatting() ? &documentMetrics : nullptr ) ); + labelFeature->setTextMetrics( QgsTextLabelFeature::calculateTextMetrics( xform, context, labelFont, *labelFontMetrics, labelFont.letterSpacing(), labelFont.wordSpacing(), labelText, evaluatedFormat.allowHtmlFormatting() ? &doc : nullptr, evaluatedFormat.allowHtmlFormatting() ? &documentMetrics : nullptr ) ); break; } @@ -3541,7 +3589,6 @@ void QgsPalLayerSettings::parseTextFormatting( QgsRenderContext &context ) // data defined multiline text align? if ( mDataDefinedProperties.isActive( QgsPalLayerSettings::Property::MultiLineAlignment ) ) { - context.expressionContext().setOriginalValueVariable( mFormat.lineHeight() ); exprVal = mDataDefinedProperties.value( QgsPalLayerSettings::Property::MultiLineAlignment, context.expressionContext() ); if ( !QgsVariantUtils::isNull( exprVal ) ) { diff --git a/src/core/labeling/qgspallabeling.h b/src/core/labeling/qgspallabeling.h index 78d1be339e38..7fa69a83469f 100644 --- a/src/core/labeling/qgspallabeling.h +++ b/src/core/labeling/qgspallabeling.h @@ -638,7 +638,7 @@ class CORE_EXPORT QgsPalLayerSettings */ #ifndef SIP_RUN void calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f = nullptr, QgsRenderContext *context = nullptr, double *rotatedLabelX SIP_OUT = nullptr, double *rotatedLabelY SIP_OUT = nullptr, - QgsTextDocument *document = nullptr, QgsTextDocumentMetrics *documentMetrics = nullptr, QRectF *outerBounds = nullptr ); + QgsTextFormat *format = nullptr, QgsTextDocument *document = nullptr, QgsTextDocumentMetrics *documentMetrics = nullptr, QRectF *outerBounds = nullptr ); #else void calculateLabelSize( const QFontMetricsF *fm, const QString &text, double &labelX, double &labelY, const QgsFeature *f = nullptr, QgsRenderContext *context = nullptr, double *rotatedLabelX SIP_OUT = nullptr, double *rotatedLabelY SIP_OUT = nullptr ); #endif diff --git a/src/core/labeling/qgsvectorlayerlabelprovider.cpp b/src/core/labeling/qgsvectorlayerlabelprovider.cpp index 625c82185851..732d252e0b66 100644 --- a/src/core/labeling/qgsvectorlayerlabelprovider.cpp +++ b/src/core/labeling/qgsvectorlayerlabelprovider.cpp @@ -760,25 +760,22 @@ void QgsVectorLayerLabelProvider::drawLabelPrivate( pal::LabelPosition *label, Q // If we are using non-curved, HTML formatted labels then we've already precalculated the text metrics. // Otherwise we'll need to calculate them now. - bool metricsRequired = !tmpLyr.format().allowHtmlFormatting(); - if ( !metricsRequired ) + bool metricsRequired = false; + switch ( tmpLyr.placement ) { - switch ( tmpLyr.placement ) - { - case Qgis::LabelPlacement::Curved: - case Qgis::LabelPlacement::PerimeterCurved: - metricsRequired = true; - break; + case Qgis::LabelPlacement::Curved: + case Qgis::LabelPlacement::PerimeterCurved: + metricsRequired = true; + break; - case Qgis::LabelPlacement::AroundPoint: - case Qgis::LabelPlacement::OverPoint: - case Qgis::LabelPlacement::Line: - case Qgis::LabelPlacement::Horizontal: - case Qgis::LabelPlacement::Free: - case Qgis::LabelPlacement::OrderedPositionsAroundPoint: - case Qgis::LabelPlacement::OutsidePolygons: - break; - } + case Qgis::LabelPlacement::AroundPoint: + case Qgis::LabelPlacement::OverPoint: + case Qgis::LabelPlacement::Line: + case Qgis::LabelPlacement::Horizontal: + case Qgis::LabelPlacement::Free: + case Qgis::LabelPlacement::OrderedPositionsAroundPoint: + case Qgis::LabelPlacement::OutsidePolygons: + break; } if ( metricsRequired ) diff --git a/src/gui/attributetable/qgsattributetablemodel.cpp b/src/gui/attributetable/qgsattributetablemodel.cpp index 7c61243ded3a..c3c20cad7200 100644 --- a/src/gui/attributetable/qgsattributetablemodel.cpp +++ b/src/gui/attributetable/qgsattributetablemodel.cpp @@ -1053,9 +1053,8 @@ void QgsAttributeTableModel::prefetchSortData( const QString &expressionString, widgetData = getWidgetData( cache.sortFieldIndex ); } - const QgsFeatureRequest request = QgsFeatureRequest( mFeatureRequest ) - .setFlags( Qgis::FeatureRequestFlag::NoGeometry ) - .setSubsetOfAttributes( cache.sortCacheAttributes ); + const QgsFeatureRequest request = QgsFeatureRequest( mFeatureRequest ).setFlags( cache.sortCacheExpression.needsGeometry() ? Qgis::FeatureRequestFlag::NoFlags : Qgis::FeatureRequestFlag::NoGeometry ).setSubsetOfAttributes( cache.sortCacheAttributes ); + QgsFeatureIterator it = mLayerCache->getFeatures( request ); QgsFeature f; @@ -1094,9 +1093,17 @@ QString QgsAttributeTableModel::sortCacheExpression( unsigned long cacheIndex ) void QgsAttributeTableModel::setRequest( const QgsFeatureRequest &request ) { - mFeatureRequest = request; - if ( mLayer && !mLayer->isSpatial() ) - mFeatureRequest.setFlags( mFeatureRequest.flags() | Qgis::FeatureRequestFlag::NoGeometry ); + if ( ! mFeatureRequest.compare( request ) ) + { + mFeatureRequest = request; + if ( mLayer && !mLayer->isSpatial() ) + mFeatureRequest.setFlags( mFeatureRequest.flags() | Qgis::FeatureRequestFlag::NoGeometry ); + // Prefetch data for sorting, resetting all caches + for ( unsigned long i = 0; i < mSortCaches.size(); ++i ) + { + prefetchSortData( sortCacheExpression( i ), i ); + } + } } const QgsFeatureRequest &QgsAttributeTableModel::request() const diff --git a/src/gui/attributetable/qgsdualview.cpp b/src/gui/attributetable/qgsdualview.cpp index 837e4ffa433a..20e40c74550e 100644 --- a/src/gui/attributetable/qgsdualview.cpp +++ b/src/gui/attributetable/qgsdualview.cpp @@ -140,10 +140,13 @@ void QgsDualView::init( QgsVectorLayer *layer, QgsMapCanvas *mapCanvas, const Qg // create an empty form to find out if it needs geometry or not const QgsAttributeForm emptyForm( mLayer, QgsFeature(), mEditorContext ); + const QgsExpression sortingExpression = QgsExpression( mConfig.sortExpression() ); + const bool needsGeometry = mLayer->conditionalStyles()->rulesNeedGeometry() || !( request.flags() & Qgis::FeatureRequestFlag::NoGeometry ) || ( request.spatialFilterType() != Qgis::SpatialFilterType::NoFilter ) - || emptyForm.needsGeometry(); + || emptyForm.needsGeometry() + || sortingExpression.needsGeometry(); initLayerCache( needsGeometry ); initModels( mapCanvas, request, loadFeatures ); @@ -336,16 +339,13 @@ void QgsDualView::setFilterMode( QgsAttributeTableFilterModel::FilterMode filter // create an empty form to find out if it needs geometry or not const QgsAttributeForm emptyForm( mLayer, QgsFeature(), mEditorContext ); - const bool needsGeometry = ( filterMode == QgsAttributeTableFilterModel::ShowVisible ) || emptyForm.needsGeometry(); + const bool needsGeometry = ( filterMode == QgsAttributeTableFilterModel::ShowVisible ) || emptyForm.needsGeometry() || QgsExpression( mConfig.sortExpression() ).needsGeometry(); const bool requiresTableReload = ( request.filterType() != Qgis::Qgis::FeatureRequestFilterType::NoFilter || request.spatialFilterType() != Qgis::SpatialFilterType::NoFilter ) // previous request was subset || ( needsGeometry && request.flags() & Qgis::FeatureRequestFlag::NoGeometry ) // no geometry for last request || ( mMasterModel->rowCount() == 0 ); // no features - if ( !needsGeometry ) - request.setFlags( request.flags() | Qgis::FeatureRequestFlag::NoGeometry ); - else - request.setFlags( request.flags() & ~( static_cast< int >( Qgis::FeatureRequestFlag::NoGeometry ) ) ); + request.setFlags( request.flags().setFlag( Qgis::FeatureRequestFlag::NoGeometry, !needsGeometry ) ); request.setFilterFids( QgsFeatureIds() ); request.setFilterRect( QgsRectangle() ); request.disableFilter(); @@ -1135,7 +1135,6 @@ bool QgsDualView::modifySort() config.setSortExpression( QString() ); } - setAttributeTableConfig( config ); return true; } else @@ -1334,7 +1333,13 @@ void QgsDualView::setAttributeTableConfig( const QgsAttributeTableConfig &config mFilterModel->setAttributeTableConfig( mConfig ); mTableView->setAttributeTableConfig( mConfig ); const QgsAttributeList attributes { requiredAttributes( mLayer ) }; - QgsFeatureRequest request { mMasterModel->request() }; + QgsFeatureRequest request = mMasterModel->request(); + // if the sort expression needs geometry reset the flag + if ( QgsExpression( config.sortExpression() ).needsGeometry() ) + { + mLayerCache->setCacheGeometry( true ); + request.setFlags( request.flags().setFlag( Qgis::FeatureRequestFlag::NoGeometry, false ) ); + } request.setSubsetOfAttributes( attributes ); mMasterModel->setRequest( request ); mLayerCache->setCacheSubsetOfAttributes( attributes ); @@ -1342,6 +1347,7 @@ void QgsDualView::setAttributeTableConfig( const QgsAttributeTableConfig &config void QgsDualView::setSortExpression( const QString &sortExpression, Qt::SortOrder sortOrder ) { + if ( sortExpression.isNull() ) mFilterModel->sort( -1 ); else diff --git a/src/gui/vector/qgsattributeactiondialog.cpp b/src/gui/vector/qgsattributeactiondialog.cpp index aabc5b30cc41..450be0491d8f 100644 --- a/src/gui/vector/qgsattributeactiondialog.cpp +++ b/src/gui/vector/qgsattributeactiondialog.cpp @@ -52,6 +52,7 @@ QgsAttributeActionDialog::QgsAttributeActionDialog( const QgsActionManager &acti connect( mMoveDownButton, &QAbstractButton::clicked, this, &QgsAttributeActionDialog::moveDown ); connect( mRemoveButton, &QAbstractButton::clicked, this, &QgsAttributeActionDialog::remove ); connect( mAddButton, &QAbstractButton::clicked, this, &QgsAttributeActionDialog::insert ); + connect( mDuplicateButton, &QAbstractButton::clicked, this, &QgsAttributeActionDialog::duplicate ); connect( mAddDefaultActionsButton, &QAbstractButton::clicked, this, &QgsAttributeActionDialog::addDefaultActions ); init( actions, mLayer->attributeTableConfig() ); @@ -296,6 +297,35 @@ void QgsAttributeActionDialog::insert() } } +void QgsAttributeActionDialog::duplicate() +{ + // Add the action details as a new row in the table. + const int pos = mAttributeActionTable->rowCount(); + const int row = mAttributeActionTable->currentRow(); + + QgsAttributeActionPropertiesDialog dlg( + static_cast( mAttributeActionTable->item( row, Type )->data( Role::ActionType ).toInt() ), + mAttributeActionTable->item( row, Description )->text(), + mAttributeActionTable->item( row, ShortTitle )->text(), + mAttributeActionTable->verticalHeaderItem( row )->data( Qt::UserRole ).toString(), + mAttributeActionTable->item( row, ActionText )->data( Qt::UserRole ).toString(), + mAttributeActionTable->item( row, Capture )->checkState() == Qt::Checked, + mAttributeActionTable->item( row, ActionScopes )->data( Qt::UserRole ).value>(), + mAttributeActionTable->item( row, NotificationMessage )->text(), + mAttributeActionTable->item( row, EnabledOnlyWhenEditable )->checkState() == Qt::Checked, + mLayer + ); + + dlg.setWindowTitle( tr( "Duplicate Action" ) ); + + if ( dlg.exec() ) + { + const QString name = uniqueName( dlg.description() ); + + insertRow( pos, dlg.type(), name, dlg.actionText(), dlg.iconPath(), dlg.capture(), dlg.shortTitle(), dlg.actionScopes(), dlg.notificationMessage(), dlg.isEnabledOnlyWhenEditable() ); + } +} + void QgsAttributeActionDialog::updateButtons() { QList selection = mAttributeActionTable->selectedItems(); @@ -314,6 +344,7 @@ void QgsAttributeActionDialog::updateButtons() } mRemoveButton->setEnabled( hasSelection ); + mDuplicateButton->setEnabled( hasSelection ); } void QgsAttributeActionDialog::addDefaultActions() diff --git a/src/gui/vector/qgsattributeactiondialog.h b/src/gui/vector/qgsattributeactiondialog.h index 87a7b5478aaf..24cc48797af3 100644 --- a/src/gui/vector/qgsattributeactiondialog.h +++ b/src/gui/vector/qgsattributeactiondialog.h @@ -81,6 +81,7 @@ class GUI_EXPORT QgsAttributeActionDialog: public QWidget, private Ui::QgsAttrib void moveDown(); void remove(); void insert(); + void duplicate(); void addDefaultActions(); void itemDoubleClicked( QTableWidgetItem *item ); void updateButtons(); diff --git a/src/ui/qgsattributeactiondialogbase.ui b/src/ui/qgsattributeactiondialogbase.ui index f01ec84972fe..0e618877069c 100644 --- a/src/ui/qgsattributeactiondialogbase.ui +++ b/src/ui/qgsattributeactiondialogbase.ui @@ -68,6 +68,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -88,7 +101,35 @@ + + + + Add a new action + + + + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + + + + Duplicate an action + + + + + + + :/images/themes/default/mActionDuplicateLayout.svg:/images/themes/default/mActionDuplicateLayout.svg + + + + Qt::Horizontal @@ -101,7 +142,7 @@ - + Create Default Actions @@ -128,7 +169,7 @@ - + @@ -193,33 +234,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Add a new action - - - - - - - :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg - - - @@ -273,6 +287,7 @@ mMoveDownButton mRemoveButton mAddButton + mDuplicateButton mAddDefaultActionsButton mShowInAttributeTable mAttributeTableWidgetType diff --git a/tests/src/core/geometry/testqgscompoundcurve.cpp b/tests/src/core/geometry/testqgscompoundcurve.cpp index 9659863c2ea3..25f44dcb69db 100644 --- a/tests/src/core/geometry/testqgscompoundcurve.cpp +++ b/tests/src/core/geometry/testqgscompoundcurve.cpp @@ -1488,7 +1488,7 @@ void TestQgsCompoundCurve::deleteVertex() cc.deleteVertex( QgsVertexId( 0, 0, 2 ) ); QCOMPARE( cc.numPoints(), 0 ); - // two lines + // two lines, small line first and long line second QgsLineString ls; ls.setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 1, 2, 2, 3 ) << QgsPoint( Qgis::WkbType::PointZM, 11, 12, 4, 5 ) ); @@ -1506,19 +1506,20 @@ void TestQgsCompoundCurve::deleteVertex() const QgsLineString *lsPtr = dynamic_cast< const QgsLineString * >( cc.curveAt( 0 ) ); - QCOMPARE( lsPtr->numPoints(), 2 ); + QCOMPARE( lsPtr->numPoints(), 3 ); QCOMPARE( lsPtr->startPoint(), QgsPoint( Qgis::WkbType::PointZM, 1, 2, 2, 3 ) ); QCOMPARE( lsPtr->endPoint(), QgsPoint( Qgis::WkbType::PointZM, 31, 42, 4, 5 ) ); //add vertex at the end of linestring - QVERIFY( cc.insertVertex( QgsVertexId( 0, 0, 2 ), QgsPoint( Qgis::WkbType::PointZM, 35, 43, 4, 5 ) ) ); + QVERIFY( cc.insertVertex( QgsVertexId( 0, 0, 3 ), QgsPoint( Qgis::WkbType::PointZM, 35, 43, 4, 5 ) ) ); lsPtr = dynamic_cast< const QgsLineString * >( cc.curveAt( 0 ) ); - QCOMPARE( lsPtr->numPoints(), 3 ); + QCOMPARE( lsPtr->numPoints(), 4 ); QCOMPARE( lsPtr->startPoint(), QgsPoint( Qgis::WkbType::PointZM, 1, 2, 2, 3 ) ); QCOMPARE( lsPtr->endPoint(), QgsPoint( Qgis::WkbType::PointZM, 35, 43, 4, 5 ) ); + // two lines, long line first and small line second ls.setPoints( QgsPointSequence() << QgsPoint( Qgis::WkbType::PointZM, 1, 2, 2, 3 ) << QgsPoint( Qgis::WkbType::PointZM, 11, 12, 4, 5 ) << QgsPoint( Qgis::WkbType::PointZM, 21, 32, 4, 5 ) ); @@ -1535,9 +1536,22 @@ void TestQgsCompoundCurve::deleteVertex() lsPtr = dynamic_cast< const QgsLineString * >( cc.curveAt( 0 ) ); - QCOMPARE( lsPtr->numPoints(), 2 ); + QCOMPARE( lsPtr->numPoints(), 3 ); QCOMPARE( lsPtr->startPoint(), QgsPoint( Qgis::WkbType::PointZM, 1, 2, 2, 3 ) ); QCOMPARE( lsPtr->endPoint(), QgsPoint( Qgis::WkbType::PointZM, 31, 42, 4, 5 ) ); + + // small ("one-curve" i.e. 3 vertices total) CircularString followed by LineString + cc.clear(); + cs.setPoints( QgsPointSequence() << QgsPoint( 0, 0 ) << QgsPoint( 1, 1 ) << QgsPoint( 0, 2 ) ); + ls.setPoints( QgsPointSequence() << QgsPoint( 0, 2 ) << QgsPoint( 0, 3 ) << QgsPoint( 0, 4 ) ); + cc.addCurve( cs.clone() ); + cc.addCurve( ls.clone() ); + + QCOMPARE( cc.nCurves(), 2 ); + QCOMPARE( cc.numPoints(), 5 ); + QVERIFY( cc.deleteVertex( QgsVertexId( 0, 0, 2 ) ) ); + QCOMPARE( cc.nCurves(), 1 ); + QCOMPARE( cc.numPoints(), 3 ); } void TestQgsCompoundCurve::filterVertices() diff --git a/tests/src/core/geometry/testqgsgeometry.cpp b/tests/src/core/geometry/testqgsgeometry.cpp index 18e410c7a9af..744c61e06686 100644 --- a/tests/src/core/geometry/testqgsgeometry.cpp +++ b/tests/src/core/geometry/testqgsgeometry.cpp @@ -2595,6 +2595,27 @@ void TestQgsGeometry::snappedToGrid() QCOMPARE( snapped->asWkt( 5 ), QStringLiteral( "LineStringZM (68 415 11 57, 27 505 24 49, 27 406 40 32)" ) ); snapped.reset( curve.constGet()->snappedToGrid( 1, 1, 10, 10 ) ); QCOMPARE( snapped->asWkt( 5 ), QStringLiteral( "LineStringZM (68 415 10 60, 27 505 20 50, 27 406 40 30)" ) ); + + // with removal of redundant vertices + curve = QgsGeometry::fromWkt( "LineString( 68.1 415.2, 68.1 505.2, 27.1 505.2 )" ); + curve = curve.densifyByCount( 10 ); + // no removal of redundant vertices + snapped.reset( curve.constGet()->snappedToGrid( 1, 1, 0, 0, false ) ); + QCOMPARE( snapped->nCoordinates(), 23 ); + // removal of redundant vertices + snapped.reset( curve.constGet()->snappedToGrid( 1, 1, 0, 0, true ) ); + QCOMPARE( snapped->asWkt( 5 ), QStringLiteral( "LineString (68 415, 68 505, 27 505)" ) ); + curve = curve.densifyByCount( 1000 ); + snapped.reset( curve.constGet()->snappedToGrid( 1, 1, 0, 0, true ) ); + QCOMPARE( snapped->asWkt( 5 ), QStringLiteral( "LineString (68 415, 68 505, 27 505)" ) ); + + // closed linestring, where the first vertex becomes redundant + curve = QgsGeometry::fromWkt( "LineString( 68.1 415.2, 90.1 415.2, 90.1 505.2, 27.1 505.2, 27.3 414.9, 68.1 415.2 )" ); + snapped.reset( curve.constGet()->snappedToGrid( 1, 1, 0, 0, true ) ); + QCOMPARE( snapped->asWkt( 5 ), QStringLiteral( "LineString (90 415, 90 505, 27 505, 27 415, 90 415)" ) ); + curve = curve.densifyByCount( 10 ); + snapped.reset( curve.constGet()->snappedToGrid( 1, 1, 0, 0, true ) ); + QCOMPARE( snapped->asWkt( 5 ), QStringLiteral( "LineString (90 415, 90 505, 27 505, 27 415, 90 415)" ) ); } //compound curve diff --git a/tests/src/core/testqgslabelingengine.cpp b/tests/src/core/testqgslabelingengine.cpp index 807b5ce1691b..373668638265 100644 --- a/tests/src/core/testqgslabelingengine.cpp +++ b/tests/src/core/testqgslabelingengine.cpp @@ -74,7 +74,10 @@ class TestQgsLabelingEngine : public QgsTest void testCurvedLabelsHtmlFormatting(); void testCurvedPerimeterLabelsHtmlFormatting(); void testCurvedLabelsHtmlSuperSubscript(); + void testPointLabelTabs(); + void testPointLabelTabsHtml(); void testPointLabelHtmlFormatting(); + void testPointLabelHtmlFormattingDataDefinedSize(); void testCurvedLabelsWithTinySegments(); void testCurvedLabelCorrectLinePlacement(); void testCurvedLabelNegativeDistance(); @@ -119,6 +122,7 @@ class TestQgsLabelingEngine : public QgsTest void testVerticalOrientation(); void testVerticalOrientationLetterLineSpacing(); void testRotationBasedOrientationPoint(); + void testRotationBasedOrientationPointHtmlLabel(); void testRotationBasedOrientationLine(); void testMapUnitLetterSpacing(); void testMapUnitWordSpacing(); @@ -1682,6 +1686,112 @@ void TestQgsLabelingEngine::testMergingLinesWithMinimumSize() QVERIFY( imageCheck( QStringLiteral( "label_merged_minimum_size" ), img, 20 ) ); } +void TestQgsLabelingEngine::testPointLabelTabs() +{ + // test point label rendering with tab characters + QgsPalLayerSettings settings; + setDefaultLabelParams( settings ); + + QgsTextFormat format = settings.format(); + format.setSize( 40 ); + format.setColor( QColor( 0, 0, 0 ) ); + settings.setFormat( format ); + + settings.fieldName = QStringLiteral( "'test\ttabs'" ); + settings.isExpression = true; + settings.placement = Qgis::LabelPlacement::OverPoint; + settings.labelPerPart = false; + + std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + vl2->setRenderer( new QgsNullSymbolRenderer() ); + + QgsFeature f; + f.setAttributes( QgsAttributes() << 1 ); + const QgsGeometry refGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000010, 190100 5000000, 190200 5000000)" ) ); + f.setGeometry( refGeom.centroid() ); + QVERIFY( vl2->dataProvider()->addFeature( f ) ); + + vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary! + vl2->setLabelsEnabled( true ); + + // make a fake render context + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setLabelingEngineSettings( createLabelEngineSettings() ); + mapSettings.setDestinationCrs( vl2->crs() ); + + mapSettings.setOutputSize( size ); + mapSettings.setExtent( refGeom.boundingBox() ); + mapSettings.setLayers( QList() << vl2.get() ); + mapSettings.setOutputDpi( 96 ); + + QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings(); + engineSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false ); + engineSettings.setFlag( Qgis::LabelingFlag::DrawCandidates, true ); + mapSettings.setLabelingEngineSettings( engineSettings ); + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + QVERIFY( imageCheck( QStringLiteral( "label_point_tabs" ), img, 20 ) ); +} + +void TestQgsLabelingEngine::testPointLabelTabsHtml() +{ + // test point label rendering with tab characters + QgsPalLayerSettings settings; + setDefaultLabelParams( settings ); + + QgsTextFormat format = settings.format(); + format.setSize( 20 ); + format.setTabStopDistance( 11.8 ); + format.setColor( QColor( 0, 0, 0 ) ); + format.setAllowHtmlFormatting( true ); + settings.setFormat( format ); + + settings.fieldName = QStringLiteral( "'test\ttabs'" ); + settings.isExpression = true; + settings.placement = Qgis::LabelPlacement::OverPoint; + settings.labelPerPart = false; + + std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + vl2->setRenderer( new QgsNullSymbolRenderer() ); + + QgsFeature f; + f.setAttributes( QgsAttributes() << 1 ); + const QgsGeometry refGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000010, 190100 5000000, 190200 5000000)" ) ); + f.setGeometry( refGeom.centroid() ); + QVERIFY( vl2->dataProvider()->addFeature( f ) ); + + vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary! + vl2->setLabelsEnabled( true ); + + // make a fake render context + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setLabelingEngineSettings( createLabelEngineSettings() ); + mapSettings.setDestinationCrs( vl2->crs() ); + + mapSettings.setOutputSize( size ); + mapSettings.setExtent( refGeom.boundingBox() ); + mapSettings.setLayers( QList() << vl2.get() ); + mapSettings.setOutputDpi( 96 ); + + QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings(); + engineSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false ); + engineSettings.setFlag( Qgis::LabelingFlag::DrawCandidates, true ); + mapSettings.setLabelingEngineSettings( engineSettings ); + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + QVERIFY( imageCheck( QStringLiteral( "label_point_tabs" ), img, 20 ) ); +} + void TestQgsLabelingEngine::testPointLabelHtmlFormatting() { // test point label rendering with HTML formatting @@ -1735,6 +1845,60 @@ void TestQgsLabelingEngine::testPointLabelHtmlFormatting() QVERIFY( imageCheck( QStringLiteral( "label_point_html_rendering" ), img, 20 ) ); } +void TestQgsLabelingEngine::testPointLabelHtmlFormattingDataDefinedSize() +{ + // test point label rendering with HTML formatting + QgsPalLayerSettings settings; + setDefaultLabelParams( settings ); + + QgsTextFormat format = settings.format(); + format.setSize( 10 ); + format.setColor( QColor( 0, 0, 0 ) ); + format.setAllowHtmlFormatting( true ); + settings.setFormat( format ); + settings.dataDefinedProperties().setProperty( QgsPalLayerSettings::Property::Size, QgsProperty::fromExpression( QStringLiteral( "10+10" ) ) ); + + settings.fieldName = QStringLiteral( "'test HTML label

point'" ); + settings.isExpression = true; + settings.placement = Qgis::LabelPlacement::OverPoint; + settings.labelPerPart = false; + + std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + vl2->setRenderer( new QgsNullSymbolRenderer() ); + + QgsFeature f; + f.setAttributes( QgsAttributes() << 1 ); + const QgsGeometry refGeom = QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000010, 190100 5000000, 190200 5000000)" ) ); + f.setGeometry( refGeom.centroid() ); + QVERIFY( vl2->dataProvider()->addFeature( f ) ); + + vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary! + vl2->setLabelsEnabled( true ); + + // make a fake render context + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setLabelingEngineSettings( createLabelEngineSettings() ); + mapSettings.setDestinationCrs( vl2->crs() ); + + mapSettings.setOutputSize( size ); + mapSettings.setExtent( refGeom.boundingBox() ); + mapSettings.setLayers( QList() << vl2.get() ); + mapSettings.setOutputDpi( 96 ); + + QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings(); + engineSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false ); + engineSettings.setFlag( Qgis::LabelingFlag::DrawCandidates, true ); + mapSettings.setLabelingEngineSettings( engineSettings ); + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + QVERIFY( imageCheck( QStringLiteral( "label_point_html_rendering" ), img, 20 ) ); +} + void TestQgsLabelingEngine::testCurvedLabelsHtmlSuperSubscript() { // test line label rendering with HTML formatting @@ -4956,6 +5120,54 @@ void TestQgsLabelingEngine::testRotationBasedOrientationPoint() vl->setLabeling( nullptr ); } +void TestQgsLabelingEngine::testRotationBasedOrientationPointHtmlLabel() +{ + const QSize size( 640, 480 ); + QgsMapSettings mapSettings; + mapSettings.setLabelingEngineSettings( createLabelEngineSettings() ); + mapSettings.setOutputSize( size ); + mapSettings.setExtent( vl->extent() ); + mapSettings.setLayers( QList() << vl ); + mapSettings.setOutputDpi( 96 ); + + // first render the map and labeling separately + + QgsMapRendererSequentialJob job( mapSettings ); + job.start(); + job.waitForFinished(); + + QImage img = job.renderedImage(); + + QPainter p( &img ); + QgsRenderContext context = QgsRenderContext::fromMapSettings( mapSettings ); + context.setPainter( &p ); + + QgsPalLayerSettings settings; + settings.fieldName = QStringLiteral( "'' || \"Class\" || ''" ); + settings.isExpression = true; + setDefaultLabelParams( settings ); + settings.dataDefinedProperties().setProperty( QgsPalLayerSettings::Property::LabelRotation, QgsProperty::fromExpression( QStringLiteral( "\"Heading\"" ) ) ); + QgsTextFormat format = settings.format(); + format.setSize( 26 ); + format.setOrientation( Qgis::TextOrientation::RotationBased ); + format.setAllowHtmlFormatting( true ); + settings.setFormat( format ); + + vl->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary! + vl->setLabelsEnabled( true ); + + QgsDefaultLabelingEngine engine; + engine.setMapSettings( mapSettings ); + engine.addProvider( new QgsVectorLayerLabelProvider( vl, QString(), true, &settings ) ); + engine.run( context ); + + p.end(); + + QVERIFY( imageCheck( "labeling_rotation_based_orientation_point", img, 20 ) ); + + vl->setLabeling( nullptr ); +} + void TestQgsLabelingEngine::testRotationBasedOrientationLine() { const QString filename = QStringLiteral( TEST_DATA_DIR ) + "/lines.shp"; diff --git a/tests/src/gui/testqgsdualview.cpp b/tests/src/gui/testqgsdualview.cpp index 89641dbc8396..aa05d6d75bd3 100644 --- a/tests/src/gui/testqgsdualview.cpp +++ b/tests/src/gui/testqgsdualview.cpp @@ -344,6 +344,17 @@ void TestQgsDualView::testNoGeom() model = dv->masterModel(); QVERIFY( !model->layerCache()->cacheGeometry() ); QVERIFY( ( model->request().flags() & Qgis::FeatureRequestFlag::NoGeometry ) ); + + // request with NO geometry but with an ordering expression which does + req = QgsFeatureRequest().setFlags( Qgis::FeatureRequestFlag::NoGeometry ); + dv.reset( new QgsDualView() ); + dv->init( mPointsLayer, mCanvas, req ); + auto config = mPointsLayer->attributeTableConfig(); + config.setSortExpression( "$x" ); + dv->setAttributeTableConfig( config ); + model = dv->masterModel(); + QVERIFY( model->layerCache()->cacheGeometry() ); + QVERIFY( !( model->request().flags() & Qgis::FeatureRequestFlag::NoGeometry ) ); } #ifdef WITH_QTWEBKIT diff --git a/tests/src/python/test_qgsattributetablemodel.py b/tests/src/python/test_qgsattributetablemodel.py index 641626ec9819..89aab5d09f83 100644 --- a/tests/src/python/test_qgsattributetablemodel.py +++ b/tests/src/python/test_qgsattributetablemodel.py @@ -25,12 +25,13 @@ QgsGeometry, QgsMemoryProviderUtils, QgsPointXY, + QgsPoint, QgsProject, QgsVectorLayer, QgsVectorLayerCache, QgsVectorLayerExporter, ) -from qgis.gui import QgsAttributeTableModel, QgsEditorWidgetFactory, QgsGui +from qgis.gui import QgsAttributeTableModel, QgsAttributeTableFilterModel, QgsEditorWidgetFactory, QgsGui import unittest from qgis.testing import start_app, QgisTestCase @@ -401,6 +402,44 @@ def data(self, index, role): self.assertEqual(twf.widgetLoaded, 2) twf.widgetLoaded = 0 + def test_sort_requires_geometry(self): + layer = QgsVectorLayer("Linestring?field=fldint:integer", + "addfeat", "memory") + pr = layer.dataProvider() + features = list() + f = QgsFeature(layer.fields()) + f.setAttributes([2]) + f.setGeometry(QgsGeometry.fromPolyline([QgsPoint(0, 0), QgsPoint(1, 1)])) + features.append(f) + + f = QgsFeature(layer.fields()) + f.setAttributes([1]) + f.setGeometry(QgsGeometry.fromPolyline([QgsPoint(0, 0), QgsPoint(2, 2)])) + features.append(f) + + self.assertTrue(pr.addFeatures(features)) + cache = QgsVectorLayerCache(layer, 100) + am = QgsAttributeTableModel(cache) + am.loadLayer() + + fm = QgsAttributeTableFilterModel(None, am, am) + + fm.sort('"fldint"', Qt.SortOrder.AscendingOrder) + self.assertEqual(fm.data(fm.index(0, 0), Qt.ItemDataRole.DisplayRole), '1') + self.assertEqual(fm.data(fm.index(1, 0), Qt.ItemDataRole.DisplayRole), '2') + + fm.sort('"fldint"', Qt.SortOrder.DescendingOrder) + self.assertEqual(fm.data(fm.index(0, 0), Qt.ItemDataRole.DisplayRole), '2') + self.assertEqual(fm.data(fm.index(1, 0), Qt.ItemDataRole.DisplayRole), '1') + + fm.sort('$length', Qt.SortOrder.DescendingOrder) + self.assertEqual(fm.data(fm.index(0, 0), Qt.ItemDataRole.DisplayRole), '1') + self.assertEqual(fm.data(fm.index(1, 0), Qt.ItemDataRole.DisplayRole), '2') + + fm.sort('$length', Qt.SortOrder.AscendingOrder) + self.assertEqual(fm.data(fm.index(0, 0), Qt.ItemDataRole.DisplayRole), '2') + self.assertEqual(fm.data(fm.index(1, 0), Qt.ItemDataRole.DisplayRole), '1') + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/labelingengine/expected_label_absolute_line_spacing/expected_label_absolute_line_spacing_mask.png b/tests/testdata/control_images/labelingengine/expected_label_absolute_line_spacing/expected_label_absolute_line_spacing_mask.png new file mode 100644 index 000000000000..b0375bbc9a48 Binary files /dev/null and b/tests/testdata/control_images/labelingengine/expected_label_absolute_line_spacing/expected_label_absolute_line_spacing_mask.png differ diff --git a/tests/testdata/control_images/labelingengine/expected_label_curved_html_rendering/expected_label_curved_html_rendering_mask.png b/tests/testdata/control_images/labelingengine/expected_label_curved_html_rendering/expected_label_curved_html_rendering_mask.png new file mode 100644 index 000000000000..6d8d2d6bde5d Binary files /dev/null and b/tests/testdata/control_images/labelingengine/expected_label_curved_html_rendering/expected_label_curved_html_rendering_mask.png differ diff --git a/tests/testdata/control_images/labelingengine/expected_label_point_html_rendering/expected_label_point_html_rendering_mask.png b/tests/testdata/control_images/labelingengine/expected_label_point_html_rendering/expected_label_point_html_rendering_mask.png new file mode 100644 index 000000000000..4f4a28169a71 Binary files /dev/null and b/tests/testdata/control_images/labelingengine/expected_label_point_html_rendering/expected_label_point_html_rendering_mask.png differ diff --git a/tests/testdata/control_images/labelingengine/expected_label_point_tabs/expected_label_point_tabs.png b/tests/testdata/control_images/labelingengine/expected_label_point_tabs/expected_label_point_tabs.png new file mode 100644 index 000000000000..80d70259ceb8 Binary files /dev/null and b/tests/testdata/control_images/labelingengine/expected_label_point_tabs/expected_label_point_tabs.png differ diff --git a/tests/testdata/control_images/labelingengine/expected_label_point_tabs/expected_label_point_tabs_mask.png b/tests/testdata/control_images/labelingengine/expected_label_point_tabs/expected_label_point_tabs_mask.png new file mode 100644 index 000000000000..e7a0f817b89e Binary files /dev/null and b/tests/testdata/control_images/labelingengine/expected_label_point_tabs/expected_label_point_tabs_mask.png differ