Skip to content

Commit 7d600bd

Browse files
committed
[FEATURE] Use rendered symbol size as obstacle for point feature labels
Previously, only the point feature itself was treated as an obstacle for label candidates. If a large or offset symbol was used for the point, then labels were allowed to overlap this symbol without incurring the obstacle cost. Sponsored by City of Uster
1 parent 6b0e7de commit 7d600bd

File tree

7 files changed

+132
-14
lines changed

7 files changed

+132
-14
lines changed

src/core/qgsvectorlayerlabelprovider.cpp

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
#include "qgspallabeling.h"
2323
#include "qgsvectorlayer.h"
2424
#include "qgsvectorlayerfeatureiterator.h"
25+
#include "qgsrendererv2.h"
26+
#include "qgspolygonv2.h"
27+
#include "qgslinestringv2.h"
28+
#include "qgsmultipolygonv2.h"
2529

2630
#include "feature.h"
2731
#include "labelposition.h"
@@ -46,12 +50,13 @@ static void _fixQPictureDPI( QPainter* p )
4650

4751

4852
QgsVectorLayerLabelProvider::QgsVectorLayerLabelProvider( QgsVectorLayer* layer, bool withFeatureLoop, const QgsPalLayerSettings* settings, const QString& layerName )
53+
: mSettings( settings ? *settings : QgsPalLayerSettings::fromLayer( layer ) )
54+
, mLayerId( layer->id() )
55+
, mRenderer( layer->rendererV2() )
56+
, mFields( layer->fields() )
57+
, mCrs( layer->crs() )
4958
{
50-
mSettings = settings ? *settings : QgsPalLayerSettings::fromLayer( layer );
5159
mName = layerName.isEmpty() ? layer->id() : layerName;
52-
mLayerId = layer->id();
53-
mFields = layer->fields();
54-
mCrs = layer->crs();
5560

5661
if ( withFeatureLoop )
5762
{
@@ -72,9 +77,10 @@ QgsVectorLayerLabelProvider::QgsVectorLayerLabelProvider( const QgsPalLayerSetti
7277
const QgsFields& fields,
7378
const QgsCoordinateReferenceSystem& crs,
7479
QgsAbstractFeatureSource* source,
75-
bool ownsSource )
80+
bool ownsSource, QgsFeatureRendererV2* renderer )
7681
: mSettings( settings )
7782
, mLayerId( layerId )
83+
, mRenderer( renderer )
7884
, mFields( fields )
7985
, mCrs( crs )
8086
, mSource( source )
@@ -263,7 +269,13 @@ QList<QgsLabelFeature*> QgsVectorLayerLabelProvider::labelFeatures( QgsRenderCon
263269
QgsFeature fet;
264270
while ( fit.nextFeature( fet ) )
265271
{
266-
registerFeature( fet, ctx );
272+
QScopedPointer<QgsGeometry> obstacleGeometry;
273+
if ( fet.constGeometry()->type() == QGis::Point )
274+
{
275+
//point feature, use symbol bounds as obstacle
276+
obstacleGeometry.reset( getPointObstacleGeometry( fet, ctx, mRenderer ) );
277+
}
278+
registerFeature( fet, ctx, obstacleGeometry.data() );
267279
}
268280

269281
return mLabels;
@@ -277,6 +289,86 @@ void QgsVectorLayerLabelProvider::registerFeature( QgsFeature& feature, QgsRende
277289
mLabels << label;
278290
}
279291

292+
QgsGeometry* QgsVectorLayerLabelProvider::getPointObstacleGeometry( QgsFeature& fet, QgsRenderContext& context, QgsFeatureRendererV2* renderer )
293+
{
294+
if ( !fet.constGeometry() || fet.constGeometry()->isEmpty() || fet.constGeometry()->type() != QGis::Point || !renderer )
295+
return 0;
296+
297+
//calculate bounds for symbols for feature
298+
QgsSymbolV2List symbols = renderer->originalSymbolsForFeature( fet, context );
299+
300+
bool isMultiPoint = fet.constGeometry()->geometry()->nCoordinates() > 1;
301+
QgsAbstractGeometryV2* obstacleGeom = 0;
302+
if ( isMultiPoint )
303+
obstacleGeom = new QgsMultiPolygonV2();
304+
else
305+
obstacleGeom = new QgsPolygonV2();
306+
307+
// for each point
308+
for ( int i = 0; i < fet.constGeometry()->geometry()->nCoordinates(); ++i )
309+
{
310+
QRectF bounds;
311+
QgsPointV2 p = fet.constGeometry()->geometry()->vertexAt( QgsVertexId( i, 0, 0 ) );
312+
double x = p.x();
313+
double y = p.y();
314+
double z = 0; // dummy variable for coordinate transforms
315+
316+
//transform point to pixels
317+
if ( context.coordinateTransform() )
318+
{
319+
context.coordinateTransform()->transformInPlace( x, y, z );
320+
}
321+
context.mapToPixel().transformInPlace( x, y );
322+
323+
QPointF pt( x, y );
324+
Q_FOREACH ( QgsSymbolV2* symbol, symbols )
325+
{
326+
if ( symbol->type() == QgsSymbolV2::Marker )
327+
{
328+
if ( bounds.isValid() )
329+
bounds = bounds.united( static_cast< QgsMarkerSymbolV2* >( symbol )->bounds( pt, context ) );
330+
else
331+
bounds = static_cast< QgsMarkerSymbolV2* >( symbol )->bounds( pt, context );
332+
}
333+
}
334+
335+
//convert bounds to a geometry
336+
QgsLineStringV2* boundLineString = new QgsLineStringV2();
337+
boundLineString->addVertex( QgsPointV2( bounds.topLeft() ) );
338+
boundLineString->addVertex( QgsPointV2( bounds.topRight() ) );
339+
boundLineString->addVertex( QgsPointV2( bounds.bottomRight() ) );
340+
boundLineString->addVertex( QgsPointV2( bounds.bottomLeft() ) );
341+
342+
//then transform back to map units
343+
//TODO - remove when labeling is refactored to use screen units
344+
for ( int i = 0; i < boundLineString->numPoints(); ++i )
345+
{
346+
QgsPoint point = context.mapToPixel().toMapCoordinates( boundLineString->xAt( i ), boundLineString->yAt( i ) );
347+
boundLineString->setXAt( i, point.x() );
348+
boundLineString->setYAt( i, point.y() );
349+
}
350+
if ( context.coordinateTransform() )
351+
{
352+
boundLineString->transform( *context.coordinateTransform(), QgsCoordinateTransform::ReverseTransform );
353+
}
354+
boundLineString->close();
355+
356+
QgsPolygonV2* obstaclePolygon = new QgsPolygonV2();
357+
obstaclePolygon->setExteriorRing( boundLineString );
358+
359+
if ( isMultiPoint )
360+
{
361+
static_cast<QgsMultiPolygonV2*>( obstacleGeom )->addGeometry( obstaclePolygon );
362+
}
363+
else
364+
{
365+
obstacleGeom = obstaclePolygon;
366+
}
367+
}
368+
369+
return new QgsGeometry( obstacleGeom );
370+
}
371+
280372
void QgsVectorLayerLabelProvider::drawLabel( QgsRenderContext& context, pal::LabelPosition* label ) const
281373
{
282374
if ( !mSettings.drawLabels )

src/core/qgsvectorlayerlabelprovider.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider
4141
const QgsFields& fields,
4242
const QgsCoordinateReferenceSystem& crs,
4343
QgsAbstractFeatureSource* source,
44-
bool ownsSource );
44+
bool ownsSource,
45+
QgsFeatureRendererV2* renderer = 0 );
4546

4647
~QgsVectorLayerLabelProvider();
4748

@@ -72,6 +73,16 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider
7273
*/
7374
virtual void registerFeature( QgsFeature& feature, QgsRenderContext &context, QgsGeometry* obstacleGeometry = 0 );
7475

76+
/** Returns the geometry for a point feature which should be used as an obstacle for labels. This
77+
* obstacle geometry will respect the dimensions and offsets of the symbol used to render the
78+
* point, and ensures that labels will not overlap large or offset points.
79+
* @param fet point feature
80+
* @param context render context
81+
* @param renderer renderer used for layer, required to determine symbols rendered for point feature
82+
* @note added in QGIS 2.14
83+
*/
84+
static QgsGeometry* getPointObstacleGeometry( QgsFeature& fet, QgsRenderContext& context, QgsFeatureRendererV2* renderer );
85+
7586
protected:
7687
//! initialization method - called from constructors
7788
void init();
@@ -84,6 +95,8 @@ class CORE_EXPORT QgsVectorLayerLabelProvider : public QgsAbstractLabelProvider
8495
//! Layer's ID
8596
QString mLayerId;
8697

98+
QgsFeatureRendererV2* mRenderer;
99+
87100
// these are needed only if using own renderer loop
88101

89102
//! Layer's fields

src/core/qgsvectorlayerrenderer.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,18 @@ void QgsVectorLayerRenderer::drawRendererV2( QgsFeatureIterator& fit )
326326
// new labeling engine
327327
if ( mContext.labelingEngineV2() )
328328
{
329+
QScopedPointer<QgsGeometry> obstacleGeometry;
330+
if ( fet.constGeometry()->type() == QGis::Point )
331+
{
332+
obstacleGeometry.reset( QgsVectorLayerLabelProvider::getPointObstacleGeometry( fet, mContext, mRendererV2 ) );
333+
}
329334
if ( mLabelProvider )
330335
{
331-
mLabelProvider->registerFeature( fet, mContext );
336+
mLabelProvider->registerFeature( fet, mContext, obstacleGeometry.data() );
332337
}
333338
if ( mDiagramProvider )
334339
{
335-
mDiagramProvider->registerFeature( fet, mContext );
340+
mDiagramProvider->registerFeature( fet, mContext, obstacleGeometry.data() );
336341
}
337342
}
338343
}
@@ -409,13 +414,18 @@ void QgsVectorLayerRenderer::drawRendererV2Levels( QgsFeatureIterator& fit )
409414
// new labeling engine
410415
if ( mContext.labelingEngineV2() )
411416
{
417+
QScopedPointer<QgsGeometry> obstacleGeometry;
418+
if ( fet.constGeometry()->type() == QGis::Point )
419+
{
420+
obstacleGeometry.reset( QgsVectorLayerLabelProvider::getPointObstacleGeometry( fet, mContext, mRendererV2 ) );
421+
}
412422
if ( mLabelProvider )
413423
{
414-
mLabelProvider->registerFeature( fet, mContext );
424+
mLabelProvider->registerFeature( fet, mContext, obstacleGeometry.data() );
415425
}
416426
if ( mDiagramProvider )
417427
{
418-
mDiagramProvider->registerFeature( fet, mContext );
428+
mDiagramProvider->registerFeature( fet, mContext, obstacleGeometry.data() );
419429
}
420430
}
421431
}

src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ QgsSymbolV2* QgsCategorizedSymbolRendererV2::originalSymbolForFeature( QgsFeatur
243243
QVariant value;
244244
if ( mAttrNum == -1 )
245245
{
246-
Q_ASSERT( mExpression.data() );
246+
if ( !mExpression.data() )
247+
return 0;
248+
247249
value = mExpression->evaluate( &context.expressionContext() );
248250
}
249251
else

tests/src/core/testqgslabelingenginev2.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,18 @@ void TestQgsLabelingEngineV2::testBasic()
134134
QVERIFY( imageCheck( "labeling_basic", img, 0 ) );
135135

136136
// now let's test the variant when integrated into rendering loop
137+
//note the reference images are slightly different due to use of renderer for this test
137138

138139
job.start();
139140
job.waitForFinished();
140141
QImage img2 = job.renderedImage();
141142

142143
vl->setCustomProperty( "labeling/enabled", false );
143144

144-
QVERIFY( imageCheck( "labeling_basic", img2, 0 ) );
145+
QVERIFY( imageCheck( "labeling_basic_loop", img2, 0 ) );
145146
}
146147

148+
147149
void TestQgsLabelingEngineV2::testDiagrams()
148150
{
149151
QSize size( 640, 480 );

tests/src/python/test_qgspallabeling_placement.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ def test_point_placement_narrow_polygon_obstacle(self):
133133
self.removeMapLayer(polyLayer)
134134
self.layer = None
135135

136-
@skip("not yet implemented")
137136
def test_point_placement_around_obstacle_large_symbol(self):
138137
# Default point label placement with obstacle and large symbols
139138
self.layer = TestQgsPalLabeling.loadFeatureLayer('point3')

0 commit comments

Comments
 (0)