Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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 e028067
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 6 deletions.
14 changes: 14 additions & 0 deletions python/core/auto_generated/qgsmapclippingutils.sip.in
Expand Up @@ -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
};

Expand Down
8 changes: 7 additions & 1 deletion src/core/labeling/qgspallabeling.cpp
Expand Up @@ -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 )
Expand Down
50 changes: 50 additions & 0 deletions src/core/qgsmapclippingutils.cpp
Expand Up @@ -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;
}
14 changes: 14 additions & 0 deletions src/core/qgsmapclippingutils.h
Expand Up @@ -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
25 changes: 20 additions & 5 deletions src/core/qgsvectorlayerrenderer.cpp
Expand Up @@ -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 );

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() );
Expand Down Expand Up @@ -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 );
Expand All @@ -393,6 +396,9 @@ void QgsVectorLayerRenderer::drawRenderer( QgsFeatureIterator &fit )
{
mDiagramProvider->registerFeature( fet, context, obstacleGeometry );
}

if ( mApplyLabelClipGeometries )
context.setFeatureClipGeometry( QgsGeometry() );
}
}
}
Expand Down Expand Up @@ -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 ) )
Expand Down Expand Up @@ -492,6 +501,9 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureIterator &fit )
}
}

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

scopePopper.reset();

if ( features.empty() )
Expand Down Expand Up @@ -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++ )
{
Expand Down
3 changes: 3 additions & 0 deletions src/core/qgsvectorlayerrenderer.h
Expand Up @@ -165,6 +165,9 @@ class QgsVectorLayerRenderer : public QgsMapLayerRenderer
bool mApplyClipFilter = false;
QgsGeometry mClipFeatureGeom;
bool mApplyClipGeometries = false;
QgsGeometry mLabelClipFeatureGeom;
bool mApplyLabelClipGeometries = false;

};


Expand Down
70 changes: 70 additions & 0 deletions tests/src/core/testqgslabelingengine.cpp
Expand Up @@ -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
Expand Down Expand Up @@ -80,6 +82,7 @@ class TestQgsLabelingEngine : public QObject
void testMapUnitLetterSpacing();
void testMapUnitWordSpacing();
void testReferencedFields();
void testClipping();

private:
QgsVectorLayer *vl = nullptr;
Expand Down Expand Up @@ -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"
39 changes: 39 additions & 0 deletions tests/src/python/test_qgsmapclippingutils.py
Expand Up @@ -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()
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 e028067

Please sign in to comment.