qgsprofilerenderer: Add support for subsections indicator

This adds support to display vertices lines over the profile graph to
display indicator at the location of the curve's vertices.

This is achieved with mainly 2 changes:
- `QgsProfilePlotRenderer::setSubsectionsSymbol()` allows to set the
line symbol of the vertical lines
- `renderSubsectionsIndicator` generates and renders the vertical
lines. It is called by `QgsProfilePlotRenderer::renderToImage()`.
This commit is contained in:
Jean Felder 2025-03-13 21:16:36 +01:00 committed by Nyall Dawson
parent f340394465
commit 030c04c9ea
6 changed files with 190 additions and 6 deletions

View File

@ -150,6 +150,35 @@ If ``sourceId`` is empty then all sources will be rendered, otherwise only the m
Renders a portion of the profile using the specified render ``context``.
If ``sourceId`` is empty then all sources will be rendered, otherwise only the matching source will be rendered.
%End
QgsLineSymbol *subsectionsSymbol();
%Docstring
Returns the line symbol used to draw the subsections.
.. seealso:: :py:func:`setSubsectionsSymbol`
.. versionadded:: 3.44
%End
void setSubsectionsSymbol( QgsLineSymbol *symbol /Transfer/ );
%Docstring
Sets the ``symbol`` used to draw the subsections. If ``symbol`` is ``None``, the subsections are not drawn.
Ownership of ``symbol`` is transferred.
.. seealso:: :py:func:`subsectionsSymbol`
.. versionadded:: 3.44
%End
void renderSubsectionsIndicator( QgsRenderContext &context, const QRectF &plotArea, double distanceMin, double distanceMax, double zMin, double zMax );
%Docstring
Renders the vertices of the profile curve as vertical lines using the specified render ``context``.
The style of the lines the style corresponds to the symbol defined by :py:func:`~QgsProfilePlotRenderer.setSubsectionsSymbol`.
.. seealso:: :py:func:`setSubsectionsSymbol`
.. versionadded:: 3.44
%End
QgsProfileSnapResult snapPoint( const QgsProfilePoint &point, const QgsProfileSnapContext &context );

View File

@ -150,6 +150,35 @@ If ``sourceId`` is empty then all sources will be rendered, otherwise only the m
Renders a portion of the profile using the specified render ``context``.
If ``sourceId`` is empty then all sources will be rendered, otherwise only the matching source will be rendered.
%End
QgsLineSymbol *subsectionsSymbol();
%Docstring
Returns the line symbol used to draw the subsections.
.. seealso:: :py:func:`setSubsectionsSymbol`
.. versionadded:: 3.44
%End
void setSubsectionsSymbol( QgsLineSymbol *symbol /Transfer/ );
%Docstring
Sets the ``symbol`` used to draw the subsections. If ``symbol`` is ``None``, the subsections are not drawn.
Ownership of ``symbol`` is transferred.
.. seealso:: :py:func:`subsectionsSymbol`
.. versionadded:: 3.44
%End
void renderSubsectionsIndicator( QgsRenderContext &context, const QRectF &plotArea, double distanceMin, double distanceMax, double zMin, double zMax );
%Docstring
Renders the vertices of the profile curve as vertical lines using the specified render ``context``.
The style of the lines the style corresponds to the symbol defined by :py:func:`~QgsProfilePlotRenderer.setSubsectionsSymbol`.
.. seealso:: :py:func:`setSubsectionsSymbol`
.. versionadded:: 3.44
%End
QgsProfileSnapResult snapPoint( const QgsProfilePoint &point, const QgsProfileSnapContext &context );

View File

@ -316,11 +316,23 @@ QImage QgsProfilePlotRenderer::renderToImage( int width, int height, double dist
context.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
render( context, width, height, distanceMin, distanceMax, zMin, zMax, sourceId );
QRectF plotArea( QPointF( 0, 0 ), QPointF( width, height ) );
renderSubsectionsIndicator( context, plotArea, distanceMin, distanceMax, zMin, zMax );
p.end();
return res;
}
QTransform QgsProfilePlotRenderer::computeRenderTransform( double width, double height, double distanceMin, double distanceMax, double zMin, double zMax )
{
QTransform transform;
transform.translate( 0, height );
transform.scale( width / ( distanceMax - distanceMin ), -height / ( zMax - zMin ) );
transform.translate( -distanceMin, -zMin );
return transform;
}
void QgsProfilePlotRenderer::render( QgsRenderContext &context, double width, double height, double distanceMin, double distanceMax, double zMin, double zMax, const QString &sourceId )
{
QPainter *painter = context.painter();
@ -328,12 +340,7 @@ void QgsProfilePlotRenderer::render( QgsRenderContext &context, double width, do
return;
QgsProfileRenderContext profileRenderContext( context );
QTransform transform;
transform.translate( 0, height );
transform.scale( width / ( distanceMax - distanceMin ), -height / ( zMax - zMin ) );
transform.translate( -distanceMin, -zMin );
profileRenderContext.setWorldTransform( transform );
profileRenderContext.setWorldTransform( computeRenderTransform( width, height, distanceMin, distanceMax, zMin, zMax ) );
profileRenderContext.setDistanceRange( QgsDoubleRange( distanceMin, distanceMax ) );
profileRenderContext.setElevationRange( QgsDoubleRange( zMin, zMax ) );
@ -357,6 +364,42 @@ void QgsProfilePlotRenderer::render( QgsRenderContext &context, double width, do
}
}
void QgsProfilePlotRenderer::setSubsectionsSymbol( QgsLineSymbol *symbol )
{
mSubsectionsSymbol.reset( symbol );
}
QgsLineSymbol *QgsProfilePlotRenderer::subsectionsSymbol()
{
return mSubsectionsSymbol.get();
}
void QgsProfilePlotRenderer::renderSubsectionsIndicator( QgsRenderContext &context, const QRectF &plotArea, double distanceMin, double distanceMax, double zMin, double zMax )
{
QgsCurve *profileCurve = mRequest.profileCurve();
if ( !profileCurve || profileCurve->length() < 3 || !mSubsectionsSymbol )
return;
QTransform transform = computeRenderTransform( plotArea.width(), plotArea.height(), distanceMin, distanceMax, zMin, zMax );
QgsPointSequence points;
profileCurve->points( points );
QgsPoint firstPoint = points.takeFirst();
points.removeLast();
mSubsectionsSymbol->startRender( context );
double accumulatedDistance = 0.;
for ( const QgsPoint &point : points )
{
accumulatedDistance += point.distance( firstPoint );
QPointF output = transform.map( QPointF( accumulatedDistance, 0. ) );
QPolygonF polyLine( QVector<QPointF> { QPointF( output.x() + plotArea.left(), plotArea.top() ), QPointF( output.x() + plotArea.left(), plotArea.bottom() ) } );
mSubsectionsSymbol->renderPolyline( polyLine, nullptr, context );
firstPoint = point;
}
mSubsectionsSymbol->stopRender( context );
}
QgsProfileSnapResult QgsProfilePlotRenderer::snapPoint( const QgsProfilePoint &point, const QgsProfileSnapContext &context )
{
QgsProfileSnapResult bestSnapResult;

View File

@ -19,6 +19,7 @@
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgslinesymbol.h"
#include "qgsprofilerequest.h"
#include "qgsabstractprofilegenerator.h"
#include "qgsrange.h"
@ -183,6 +184,32 @@ class CORE_EXPORT QgsProfilePlotRenderer : public QObject
*/
void render( QgsRenderContext &context, double width, double height, double distanceMin, double distanceMax, double zMin, double zMax, const QString &sourceId = QString() );
/**
* Returns the line symbol used to draw the subsections.
*
* \see setSubsectionsSymbol()
* \since QGIS 3.44
*/
QgsLineSymbol *subsectionsSymbol();
/**
* Sets the \a symbol used to draw the subsections. If \a symbol is NULLPTR, the subsections are not drawn.
* Ownership of \a symbol is transferred.
*
* \see subsectionsSymbol()
* \since QGIS 3.44
*/
void setSubsectionsSymbol( QgsLineSymbol *symbol SIP_TRANSFER );
/**
* Renders the vertices of the profile curve as vertical lines using the specified render \a context.
* The style of the lines the style corresponds to the symbol defined by setSubsectionsSymbol().
*
* \see setSubsectionsSymbol()
* \since QGIS 3.44
*/
void renderSubsectionsIndicator( QgsRenderContext &context, const QRectF &plotArea, double distanceMin, double distanceMax, double zMin, double zMax );
/**
* Snap a \a point to the results.
*/
@ -216,6 +243,8 @@ class CORE_EXPORT QgsProfilePlotRenderer : public QObject
private:
static QTransform computeRenderTransform( double width, double height, double distanceMin, double distanceMax, double zMin, double zMax );
struct ProfileJob
{
QgsAbstractProfileGenerator *generator = nullptr;
@ -240,6 +269,8 @@ class CORE_EXPORT QgsProfilePlotRenderer : public QObject
enum { Idle, Generating } mStatus = Idle;
std::unique_ptr<QgsLineSymbol> mSubsectionsSymbol;
};
#endif // QGSPROFILERENDERER_H

View File

@ -2703,6 +2703,58 @@ class TestQgsVectorLayerProfileGenerator(QgisTestCase):
)
)
def testRenderProfileWithSubsections(self):
vl = QgsVectorLayer("PolygonZ?crs=EPSG:27700", "lines", "memory")
vl.setCrs(QgsCoordinateReferenceSystem())
self.assertTrue(vl.isValid())
for line in [
"PolygonZ ((321829.48893365426920354 129991.38697145861806348 1, 321847.89668515208177269 129996.63588572069420479 1, 321848.97131609614007175 129979.22330882755341008 1, 321830.31725845142500475 129978.07136809575604275 1, 321829.48893365426920354 129991.38697145861806348 1))",
"PolygonZ ((321920.00953056826256216 129924.58260190498549491 2, 321924.65299345907988027 129908.43546159457764588 2, 321904.78543491888558492 129903.99811821122420952 2, 321900.80605239619035274 129931.39860145389684476 2, 321904.84799937985371798 129931.71552911199978553 2, 321908.93646715773502365 129912.90030360443051904 2, 321914.20495146053144708 129913.67693978428724222 2, 321911.30165811872575432 129923.01272751353099011 2, 321920.00953056826256216 129924.58260190498549491 2))",
"PolygonZ ((321923.10517279652412981 129919.61521573827485554 3, 321922.23537852568551898 129928.3598982143739704 3, 321928.60423935484141111 129934.22530528216157109 3, 321929.39881197665818036 129923.29054521876969375 3, 321930.55804549407912418 129916.53248518184409477 3, 321923.10517279652412981 129919.61521573827485554 3))",
"PolygonZ ((321990.47451346553862095 129909.63588680300745182 4, 321995.04325810901354998 129891.84052284323843196 4, 321989.66826330573530868 129890.5092018858413212 4, 321990.78512359503656626 129886.49917887404444627 4, 321987.37291929306229576 129885.64982962771318853 4, 321985.2254804756375961 129893.81317058412241749 4, 321987.63158903241856024 129894.41078495365218259 4, 321984.34022761805681512 129907.57450046355370432 4, 321990.47451346553862095 129909.63588680300745182 4))",
"PolygonZ ((322103.03910495212767273 129795.91051736124791205 5, 322108.25568856322206557 129804.76113295342656784 5, 322113.29666162584908307 129803.9285887333098799 5, 322117.78645010641776025 129794.48194090687320568 5, 322103.03910495212767273 129795.91051736124791205 5))",
]:
f = QgsFeature()
f.setGeometry(QgsGeometry.fromWkt(line))
self.assertTrue(vl.dataProvider().addFeature(f))
tolerance = 0
vl.elevationProperties().setClamping(Qgis.AltitudeClamping.Absolute)
vl.elevationProperties().setExtrusionEnabled(True)
vl.elevationProperties().setExtrusionHeight(7)
fill_symbol = QgsFillSymbol.createSimple(
{"color": "#ff00ff", "outline_style": "no"}
)
vl.elevationProperties().setRespectLayerSymbology(False)
vl.elevationProperties().setProfileFillSymbol(fill_symbol)
curve = QgsLineString()
curve.fromWkt(
"LineString (321829.56510233582230285 129998.24048265765304677, 321870.42546843132004142 129896.35489447148574982, 321909.69387221138458699 129873.00611384549119975, 321915.26574031531345099 129913.07049878328689374, 321933.04265283740824088 129926.33685141168825794, 322004.9462840833584778 129896.08956741892325226, 322113.19972153112757951 129794.73463333789550234, 322150.61083594325464219 129810.65425649198004976)"
)
req = QgsProfileRequest(curve)
req.setTransformContext(self.create_transform_context())
req.setCrs(QgsCoordinateReferenceSystem())
req.setTolerance(tolerance)
plot_renderer = QgsProfilePlotRenderer([vl], req)
plot_renderer.setSubsectionsSymbol(
QgsLineSymbol.createSimple({"color": "#0000ff", "width": 1.5})
)
plot_renderer.startGeneration()
plot_renderer.waitForFinished()
res = plot_renderer.renderToImage(400, 400, 0, curve.length(), 0, 14)
self.assertTrue(
self.image_check(
"vector_profile_with_subsections",
"vector_profile_with_subsections",
res,
)
)
def doCheckPoint(
self,
request: QgsProfileRequest,