Skip to content
Permalink
Browse files

Ensure map clip region is correctly handled during labeling

We don't want labels to be positioned using unclipped feature
geometries, rather we want them to be positioned nicely on the
visible portions of features
  • Loading branch information
nyalldawson committed Jul 2, 2020
1 parent 5cbdc4c commit e0280679269fb7eaf6f38cc2db87403b7830d92b
@@ -69,6 +69,20 @@ The returned coordinates are in painter coordinates for the destination ``contex
:param shouldClip: will be set to ``True`` if the clipping path should be applied

:return: combined painter clipping region for use when rendering maps
%End

static QgsGeometry calculateLabelIntersectionGeometry( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, bool &shouldClip );
%Docstring
Returns the geometry representing the intersection of clipping ``regions`` from ``context`` which should be used to clip individual
feature geometries while registering them with labeling engines.

The returned geometry will be automatically reprojected into the same CRS as the source layer, ready for use for clipping features.

:param regions: list of clip regions which apply to the layer
:param context: a render context
:param shouldClip: will be set to ``True`` if layer's features should be clipped for labeling, i.e. one or more clipping regions applies to the layer

:return: combined clipping region for use when labeling features
%End
};

@@ -1975,11 +1975,17 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
{
unsigned int simplifyHints = simplifyMethod.simplifyHints() | QgsMapToPixelSimplifier::SimplifyEnvelope;
QgsMapToPixelSimplifier::SimplifyAlgorithm simplifyAlgorithm = static_cast< QgsMapToPixelSimplifier::SimplifyAlgorithm >( simplifyMethod.simplifyAlgorithm() );
QgsGeometry g = geom;
QgsMapToPixelSimplifier simplifier( simplifyHints, simplifyMethod.tolerance(), simplifyAlgorithm );
geom = simplifier.simplify( geom );
}

if ( !context.featureClipGeometry().isEmpty() )
{
const QgsWkbTypes::GeometryType expectedType = geom.type();
geom = geom.intersection( context.featureClipGeometry() );
geom.convertGeometryCollectionToSubclass( expectedType );
}

// whether we're going to create a centroid for polygon
bool centroidPoly = ( ( placement == QgsPalLayerSettings::AroundPoint
|| placement == QgsPalLayerSettings::OverPoint )
@@ -179,3 +179,53 @@ QPainterPath QgsMapClippingUtils::calculatePainterClipRegion( const QList<QgsMap
path.addPolygon( result.asQPolygonF() );
return path;
}

QgsGeometry QgsMapClippingUtils::calculateLabelIntersectionGeometry( const QList<QgsMapClippingRegion> &regions, const QgsRenderContext &context, bool &shouldClip )
{
QgsGeometry result;
bool first = true;
shouldClip = false;
for ( const QgsMapClippingRegion &region : regions )
{
if ( region.geometry().type() != QgsWkbTypes::PolygonGeometry )
continue;

// for labeling, we clip using either painter clip regions or intersects type regions.
// unlike feature rendering, we clip features to painter clip regions for labeling, because
// we want the label to sit within the clip region if possible
if ( region.featureClip() != QgsMapClippingRegion::FeatureClippingType::PainterClip &&
region.featureClip() != QgsMapClippingRegion::FeatureClippingType::Intersect )
continue;

shouldClip = true;
if ( first )
{
result = region.geometry();
first = false;
}
else
{
result = result.intersection( region.geometry() );
}
}

if ( !shouldClip )
return QgsGeometry();

// filter out polygon parts from result only
result.convertGeometryCollectionToSubclass( QgsWkbTypes::PolygonGeometry );

// lastly transform back to layer CRS
try
{
result.transform( context.coordinateTransform(), QgsCoordinateTransform::ReverseTransform );
}
catch ( QgsCsException & )
{
QgsDebugMsg( QStringLiteral( "Could not transform clipping region to layer CRS" ) );
shouldClip = false;
return QgsGeometry();
}

return result;
}
@@ -86,6 +86,20 @@ class CORE_EXPORT QgsMapClippingUtils
* \returns combined painter clipping region for use when rendering maps
*/
static QPainterPath calculatePainterClipRegion( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, QgsMapLayerType layerType, bool &shouldClip );

/**
* Returns the geometry representing the intersection of clipping \a regions from \a context which should be used to clip individual
* feature geometries while registering them with labeling engines.
*
* The returned geometry will be automatically reprojected into the same CRS as the source layer, ready for use for clipping features.
*
* \param regions list of clip regions which apply to the layer
* \param context a render context
* \param shouldClip will be set to TRUE if layer's features should be clipped for labeling, i.e. one or more clipping regions applies to the layer
*
* \returns combined clipping region for use when labeling features
*/
static QgsGeometry calculateLabelIntersectionGeometry( const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, bool &shouldClip );
};

#endif // QGSMAPCLIPPINGUTILS_H
@@ -194,6 +194,8 @@ bool QgsVectorLayerRenderer::render()
const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, context, QgsMapLayerType::VectorLayer, needsPainterClipPath );
if ( needsPainterClipPath )
context.painter()->setClipPath( path, Qt::IntersectClip );

mLabelClipFeatureGeom = QgsMapClippingUtils::calculateLabelIntersectionGeometry( mClippingRegions, context, mApplyLabelClipGeometries );
}
mRenderer->modifyRequestExtent( requestExtent, context );

@@ -295,11 +297,6 @@ bool QgsVectorLayerRenderer::render()
context.setVectorSimplifyMethod( vectorMethod );
}

if ( mApplyClipGeometries )
{
context.setFeatureClipGeometry( mClipFeatureGeom );
}

QgsFeatureIterator fit = mSource->getFeatures( featureRequest );
// Attach an interruption checker so that iterators that have potentially
// slow fetchFeature() implementations, such as in the WFS provider, can
@@ -357,6 +354,9 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureIterator &fit )
if ( clipEngine && !clipEngine->intersects( fet.geometry().constGet() ) )
continue; // skip features outside of clipping region

if ( mApplyClipGeometries )
context.setFeatureClipGeometry( mClipFeatureGeom );

context.expressionContext().setFeature( fet );

bool sel = context.showSelection() && mSelectedFeatureIds.contains( fet.id() );
@@ -385,6 +385,9 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureIterator &fit )
QgsExpressionContextUtils::updateSymbolScope( symbol, symbolScope );
}

if ( mApplyLabelClipGeometries )
context.setFeatureClipGeometry( mLabelClipFeatureGeom );

if ( mLabelProvider )
{
mLabelProvider->registerFeature( fet, context, obstacleGeometry, symbol );
@@ -393,6 +396,9 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureIterator &fit )
{
mDiagramProvider->registerFeature( fet, context, obstacleGeometry );
}

if ( mApplyLabelClipGeometries )
context.setFeatureClipGeometry( QgsGeometry() );
}
}
}
@@ -434,6 +440,9 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureIterator &fit )
clipEngine->prepareGeometry();
}

if ( mApplyLabelClipGeometries )
context.setFeatureClipGeometry( mLabelClipFeatureGeom );

// 1. fetch features
QgsFeature fet;
while ( fit.nextFeature( fet ) )
@@ -492,6 +501,9 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureIterator &fit )
}
}

if ( mApplyLabelClipGeometries )
context.setFeatureClipGeometry( QgsGeometry() );

scopePopper.reset();

if ( features.empty() )
@@ -519,6 +531,9 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureIterator &fit )
}
}

if ( mApplyClipGeometries )
context.setFeatureClipGeometry( mClipFeatureGeom );

// 2. draw features in correct order
for ( int l = 0; l < levels.count(); l++ )
{
@@ -165,6 +165,9 @@ class QgsVectorLayerRenderer : public QgsMapLayerRenderer
bool mApplyClipFilter = false;
QgsGeometry mClipFeatureGeom;
bool mApplyClipGeometries = false;
QgsGeometry mLabelClipFeatureGeom;
bool mApplyLabelClipGeometries = false;

};


@@ -28,6 +28,8 @@
#include "qgsmultirenderchecker.h"
#include "qgsfontutils.h"
#include "qgsnullsymbolrenderer.h"
#include "qgssinglesymbolrenderer.h"
#include "qgssymbol.h"
#include "pointset.h"

class TestQgsLabelingEngine : public QObject
@@ -80,6 +82,7 @@ class TestQgsLabelingEngine : public QObject
void testMapUnitLetterSpacing();
void testMapUnitWordSpacing();
void testReferencedFields();
void testClipping();

private:
QgsVectorLayer *vl = nullptr;
@@ -2690,5 +2693,72 @@ void TestQgsLabelingEngine::testReferencedFields()
QCOMPARE( settings.referencedFields( QgsRenderContext() ), QSet<QString>() << QStringLiteral( "hello" ) << QStringLiteral( "world" ) << QStringLiteral( "my_dd_size" ) );
}

void TestQgsLabelingEngine::testClipping()
{
QgsPalLayerSettings settings;
setDefaultLabelParams( settings );

QgsTextFormat format = settings.format();
format.setSize( 12 );
format.setSizeUnit( QgsUnitTypes::RenderPoints );
format.setColor( QColor( 0, 0, 0 ) );
settings.setFormat( format );

settings.fieldName = QStringLiteral( "Name" );
settings.placement = QgsPalLayerSettings::Line;

const QString filename = QStringLiteral( TEST_DATA_DIR ) + "/lines.shp";
std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( filename, QStringLiteral( "lines" ), QStringLiteral( "ogr" ) ) );

QgsStringMap props;
props.insert( QStringLiteral( "outline_color" ), QStringLiteral( "#487bb6" ) );
props.insert( QStringLiteral( "outline_width" ), QStringLiteral( "1" ) );
std::unique_ptr< QgsLineSymbol > symbol( QgsLineSymbol::createSimple( props ) );
vl2->setRenderer( new QgsSingleSymbolRenderer( symbol.release() ) );

vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary!
vl2->setLabelsEnabled( true );

// make a fake render context
QSize size( 640, 480 );
QgsMapSettings mapSettings;
mapSettings.setLabelingEngineSettings( createLabelEngineSettings() );
mapSettings.setDestinationCrs( vl2->crs() );

mapSettings.setOutputSize( size );
mapSettings.setExtent( QgsRectangle( -117.543, 49.438, -82.323, 21.839 ) );
mapSettings.setLayers( QList<QgsMapLayer *>() << vl2.get() );
mapSettings.setOutputDpi( 96 );

QgsMapClippingRegion region1( QgsGeometry::fromWkt( "Polygon ((-92 45, -99 36, -94 29, -82 29, -81 45, -92 45))" ) );
region1.setFeatureClip( QgsMapClippingRegion::FeatureClippingType::Intersect );
mapSettings.addClippingRegion( region1 );

QgsMapClippingRegion region2( QgsGeometry::fromWkt( "Polygon ((-85 36, -85 46, -107 47, -108 28, -85 28, -85 36))" ) );
region2.setFeatureClip( QgsMapClippingRegion::FeatureClippingType::PainterClip );
mapSettings.addClippingRegion( region2 );

QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings();
engineSettings.setFlag( QgsLabelingEngineSettings::UsePartialCandidates, false );
//engineSettings.setFlag( QgsLabelingEngineSettings::DrawCandidates, true );
mapSettings.setLabelingEngineSettings( engineSettings );

QgsMapRendererSequentialJob job( mapSettings );
job.start();
job.waitForFinished();

QImage img = job.renderedImage();
QVERIFY( imageCheck( QStringLiteral( "label_feature_clipping" ), img, 20 ) );

// also check with symbol levels
vl2->renderer()->setUsingSymbolLevels( true );
QgsMapRendererSequentialJob job2( mapSettings );
job2.start();
job2.waitForFinished();

img = job2.renderedImage();
QVERIFY( imageCheck( QStringLiteral( "label_feature_clipping" ), img, 20 ) );
}

QGSTEST_MAIN( TestQgsLabelingEngine )
#include "testqgslabelingengine.moc"
@@ -155,6 +155,45 @@ def testPainterClipPath(self):
self.assertTrue(should_clip)
self.assertEqual(QgsGeometry.fromQPolygonF(path.toFillPolygon()).asWkt(0), 'Polygon ((98 77, 98 77, 98 77, 98 77, 98 77))')

def testLabelIntersectionGeometry(self):
region = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))'))
region.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.Intersect)
region2 = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 0.1 0, 0.1 2, 0 2, 0 0))'))
region2.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.Intersects)
region3 = QgsMapClippingRegion(QgsGeometry.fromWkt('Polygon((0 0, 0.1 0, 0.1 2, 0 2, 0 0))'))
region3.setFeatureClip(QgsMapClippingRegion.FeatureClippingType.PainterClip)

rc = QgsRenderContext()

geom, should_clip = QgsMapClippingUtils.calculateLabelIntersectionGeometry([], rc)
self.assertFalse(should_clip)
self.assertTrue(geom.isNull())

geom, should_clip = QgsMapClippingUtils.calculateLabelIntersectionGeometry([region], rc)
self.assertTrue(should_clip)
self.assertEqual(geom.asWkt(1), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))')

# region2 is a Intersects type clipping region, should not apply here
geom, should_clip = QgsMapClippingUtils.calculateLabelIntersectionGeometry([region2], rc)
self.assertFalse(should_clip)
self.assertTrue(geom.isNull())

geom, should_clip = QgsMapClippingUtils.calculateLabelIntersectionGeometry([region, region2], rc)
self.assertTrue(should_clip)
self.assertEqual(geom.asWkt(1), 'Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))')

# region3 is a PainterClip type clipping region, MUST be applied for labels
geom, should_clip = QgsMapClippingUtils.calculateLabelIntersectionGeometry([region, region2, region3], rc)
self.assertTrue(should_clip)
self.assertEqual(geom.asWkt(1), 'Polygon ((0.1 0, 0 0, 0 1, 0.1 1, 0.1 0))')

rc.setCoordinateTransform(
QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:3857'), QgsCoordinateReferenceSystem('EPSG:4326'),
QgsProject.instance()))
geom, should_clip = QgsMapClippingUtils.calculateLabelIntersectionGeometry([region, region3], rc)
self.assertTrue(should_clip)
self.assertEqual(geom.asWkt(0), 'Polygon ((11132 0, 0 0, 0 111325, 11132 111325, 11132 0))')


if __name__ == '__main__':
unittest.main()
Binary file not shown.

0 comments on commit e028067

Please sign in to comment.
You can’t perform that action at this time.