Skip to content

Commit

Permalink
Use labeling engine to render linear referencing labels
Browse files Browse the repository at this point in the history
Where possible, we now use the labeling engine to register and
render labels from a linear referencing symbol layer. This ensures
that the linear referencing labels correctly participate in
the map labeling problem, including forcing other map labels
to be repositioned to avoid overlaps.
  • Loading branch information
nyalldawson committed Sep 14, 2024
1 parent 957de0c commit beda21c
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 6 deletions.
148 changes: 144 additions & 4 deletions src/core/symbology/qgslinearreferencingsymbollayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,99 @@
#include "qgsgeometryutils.h"
#include "qgsunittypes.h"
#include "qgssymbollayerutils.h"
#include "qgstextlabelfeature.h"
#include "qgsgeos.h"
#include "qgspallabeling.h"
#include "labelposition.h"
#include "feature.h"

///@cond PRIVATE
class QgsTextLabelFeatureWithFormat : public QgsTextLabelFeature
{
public:
QgsTextLabelFeatureWithFormat( QgsFeatureId id, geos::unique_ptr geometry, QSizeF size, const QgsTextFormat &format )
: QgsTextLabelFeature( id, std::move( geometry ), size )
, mFormat( format )
{}

QgsTextFormat mFormat;

};

class QgsLinearReferencingSymbolLayerLabelProvider final : public QgsAbstractLabelProvider
{
public:
QgsLinearReferencingSymbolLayerLabelProvider()
: QgsAbstractLabelProvider( nullptr )
{
mPlacement = Qgis::LabelPlacement::OverPoint;
mFlags |= DrawLabels;

// always consider these labels highest priority
// TODO possibly expose as a setting for the symbol layer?
mPriority = 0;
}

~QgsLinearReferencingSymbolLayerLabelProvider()
{
qDeleteAll( mLabels );
}

void addLabel( const QPointF &painterPoint, double angleRadians, const QString &text, QgsRenderContext &context, const QgsTextFormat &format )
{
// labels must be registered in destination map units
QgsPoint mapPoint( painterPoint );
mapPoint.transform( context.mapToPixel().transform().inverted() );

QgsTextDocument doc;
if ( format.allowHtmlFormatting() && !text.isEmpty() )
{
doc = QgsTextDocument::fromHtml( QStringList() << text );
}
else
{
doc = QgsTextDocument::fromPlainText( { text } );
}
QgsTextDocumentMetrics documentMetrics = QgsTextDocumentMetrics::calculateMetrics( doc, format, context );
const QSizeF size = documentMetrics.documentSize( Qgis::TextLayoutMode::Point, Qgis::TextOrientation::Horizontal );

double uPP = context.mapToPixel().mapUnitsPerPixel();
std::unique_ptr< QgsTextLabelFeatureWithFormat > feature = std::make_unique< QgsTextLabelFeatureWithFormat >( mLabels.size(),
QgsGeos::asGeos( &mapPoint ), QSizeF( size.width() * uPP, size.height() * uPP ), format );

feature->setDocument( doc, documentMetrics );
feature->setFixedAngle( angleRadians );
feature->setHasFixedAngle( true );
// above right
// TODO: we could potentially expose this, and use a non-fixed mode to allow fallback positions
feature->setQuadOffset( QPointF( 1, 1 ) );

mLabels.append( feature.release() );
}

QList<QgsLabelFeature *> labelFeatures( QgsRenderContext & ) final
{
return mLabels;
}

void drawLabel( QgsRenderContext &context, pal::LabelPosition *label ) const final
{
// as per vector label rendering...
QgsMapToPixel xform = context.mapToPixel();
xform.setMapRotation( 0, 0, 0 );
const QPointF outPt = context.mapToPixel().transform( label->getX(), label->getY() ).toQPointF();

QgsTextLabelFeatureWithFormat *lf = qgis::down_cast<QgsTextLabelFeatureWithFormat *>( label->getFeaturePart()->feature() );
QgsTextRenderer::drawDocument( outPt,
lf->mFormat, lf->document(), lf->documentMetrics(), context, Qgis::TextHorizontalAlignment::Left,
label->getAlpha() );
}

private:
QList<QgsLabelFeature *> mLabels;

};
///@endcond

QgsLinearReferencingSymbolLayer::QgsLinearReferencingSymbolLayer()
: QgsLineSymbolLayer()
Expand Down Expand Up @@ -233,6 +326,14 @@ void QgsLinearReferencingSymbolLayer::startRender( QgsSymbolRenderContext &conte

mMarkerSymbol->startRender( context.renderContext(), context.fields() );
}

if ( QgsLabelingEngine *labelingEngine = context.renderContext().labelingEngine() )
{
// rendering with a labeling engine. In this scenario we will register rendered text as labels, so that they participate in the labeling problem
// for the map
QgsLinearReferencingSymbolLayerLabelProvider *provider = new QgsLinearReferencingSymbolLayerLabelProvider();
mLabelProviderId = labelingEngine->addProvider( provider );
}
}

void QgsLinearReferencingSymbolLayer::stopRender( QgsSymbolRenderContext &context )
Expand Down Expand Up @@ -698,8 +799,16 @@ void QgsLinearReferencingSymbolLayer::renderPolylineInterval( const QgsLineStrin
return;
}

QgsLinearReferencingSymbolLayerLabelProvider *labelProvider = nullptr;
if ( QgsLabelingEngine *labelingEngine = context.renderContext().labelingEngine() )
{
// rendering with a labeling engine. In this scenario we will register rendered text as labels, so that they participate in the labeling problem
// for the map
labelProvider = qgis::down_cast< QgsLinearReferencingSymbolLayerLabelProvider * >( labelingEngine->providerById( mLabelProviderId ) );
}

func( line, painterUnitsGeometry.get(), emitFirstPoint, distance, averageAngleLengthPainterUnits, [&context, &numericContext, skipMultiples, showMarker,
labelOffsetPainterUnits, hasZ, hasM, this]( double x, double y, double z, double m, double distanceFromStart, double angle ) -> bool
labelOffsetPainterUnits, hasZ, hasM, labelProvider, this]( double x, double y, double z, double m, double distanceFromStart, double angle ) -> bool
{
if ( context.renderContext().renderingStopped() )
return false;
Expand Down Expand Up @@ -742,7 +851,19 @@ void QgsLinearReferencingSymbolLayer::renderPolylineInterval( const QgsLineStrin
const double dy = labelOffsetPainterUnits.x() * std::cos( angleRadians + M_PI_2 )
+ labelOffsetPainterUnits.y() * std::cos( angleRadians );

QgsTextRenderer::drawText( QPointF( pt.x() + dx, pt.y() + dy ), angleRadians, Qgis::TextHorizontalAlignment::Left, { mNumericFormat->formatDouble( labelValue, numericContext ) }, context.renderContext(), mTextFormat );
const QString text = mNumericFormat->formatDouble( labelValue, numericContext );
if ( !labelProvider )
{
// render text directly
QgsTextRenderer::drawText( QPointF( pt.x() + dx, pt.y() + dy ), angleRadians, Qgis::TextHorizontalAlignment::Left, { text }, context.renderContext(), mTextFormat );
}
else
{
// register as a label
labelProvider->addLabel(
QPointF( pt.x() + dx, pt.y() + dy ), angleRadians, text, context.renderContext(), mTextFormat
);
}

return true;
} );
Expand All @@ -758,6 +879,14 @@ void QgsLinearReferencingSymbolLayer::renderPolylineVertex( const QgsLineString
QgsNumericFormatContext numericContext;
numericContext.setExpressionContext( context.renderContext().expressionContext() );

QgsLinearReferencingSymbolLayerLabelProvider *labelProvider = nullptr;
if ( QgsLabelingEngine *labelingEngine = context.renderContext().labelingEngine() )
{
// rendering with a labeling engine. In this scenario we will register rendered text as labels, so that they participate in the labeling problem
// for the map
labelProvider = qgis::down_cast< QgsLinearReferencingSymbolLayerLabelProvider * >( labelingEngine->providerById( mLabelProviderId ) );
}

const double *xData = line->xData();
const double *yData = line->yData();
const double *zData = line->is3D() ? line->zData() : nullptr;
Expand Down Expand Up @@ -918,8 +1047,19 @@ void QgsLinearReferencingSymbolLayer::renderPolylineVertex( const QgsLineString
if ( !labelVertex )
continue;

QgsTextRenderer::drawText( QPointF( pt.x() + dx, pt.y() + dy ), angleRadians, Qgis::TextHorizontalAlignment::Left, { mNumericFormat->formatDouble( labelValue, numericContext ) }, context.renderContext(), mTextFormat );

const QString text = mNumericFormat->formatDouble( labelValue, numericContext );
if ( !labelProvider )
{
// render text directly
QgsTextRenderer::drawText( QPointF( pt.x() + dx, pt.y() + dy ), angleRadians, Qgis::TextHorizontalAlignment::Left, { text }, context.renderContext(), mTextFormat );
}
else
{
// register as a label
labelProvider->addLabel(
QPointF( pt.x() + dx, pt.y() + dy ), angleRadians, text, context.renderContext(), mTextFormat
);
}
prevX = thisX;
prevY = thisY;
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/symbology/qgslinearreferencingsymbollayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ class CORE_EXPORT QgsLinearReferencingSymbolLayer : public QgsLineSymbolLayer
Qgis::RenderUnit mAverageAngleLengthUnit = Qgis::RenderUnit::Millimeters;
QgsMapUnitScale mAverageAngleLengthMapUnitScale;

QString mLabelProviderId;

};

#endif // QGSLINEARREFERENCINGSYMBOLLAYER_H
49 changes: 47 additions & 2 deletions tests/src/python/test_qgslinearreferencingsymbollayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"""
import unittest

from qgis.PyQt.QtCore import QPointF
from qgis.PyQt.QtCore import QPointF, QSize
from qgis.PyQt.QtGui import QColor, QImage, QPainter
from qgis.core import (
Qgis,
Expand All @@ -30,7 +30,9 @@
QgsFontUtils,
QgsBasicNumericFormat,
QgsMarkerSymbol,
QgsFillSymbol
QgsFillSymbol,
QgsVectorLayer,
QgsSingleSymbolRenderer
)
from qgis.testing import start_app, QgisTestCase

Expand Down Expand Up @@ -77,6 +79,49 @@ def test_distance_2d(self):
)
)

def test_render_using_label_engine(self):
s = QgsLineSymbol.createSimple(
{'outline_color': '#ff0000', 'outline_width': '2'})

linear_ref = QgsLinearReferencingSymbolLayer()
linear_ref.setPlacement(
Qgis.LinearReferencingPlacement.IntervalCartesian2D)
linear_ref.setInterval(1)

font = QgsFontUtils.getStandardTestFont('Bold', 18)
text_format = QgsTextFormat.fromQFont(font)
text_format.setColor(QColor(255, 255, 255))
linear_ref.setTextFormat(text_format)

linear_ref.setLabelOffset(QPointF(3, -1))

s.appendSymbolLayer(linear_ref)

layer = QgsVectorLayer('LineString', 'test', 'memory')
feature = QgsFeature()
geom = QgsGeometry.fromWkt('MultiLineStringZM ((6 2 0.2 1.2, 9 2 0.7 0.2, 9 3 0.4 0, 11 5 0.8 0.4))')
feature.setGeometry(geom)
layer.dataProvider().addFeature(feature)
layer.setRenderer(QgsSingleSymbolRenderer(s))

ms = QgsMapSettings()
ms.setLayers([layer])
ms.setDestinationCrs(layer.crs())
extent = geom.get().boundingBox()
extent = extent.buffered((extent.height() + extent.width()) / 20.0)
ms.setExtent(extent)
ms.setOutputSize(QSize(800, 800))

self.assertTrue(
self.render_map_settings_check(
'labeling_engine',
'labeling_engine',
ms,
color_tolerance=2,
allowed_mismatch=20
)
)

def test_distance_2d_with_z(self):
s = QgsLineSymbol.createSimple(
{'outline_color': '#ff0000', 'outline_width': '2'})
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit beda21c

Please sign in to comment.