Skip to content

Commit 03f9e1b

Browse files
committed
[pal] Never treat features as obstacles for their own labels
Fixes issues like rule based labelling registering two labels for a single feature which was resulting in each label colliding with the feature's geometry registered by the other label. This resulted in poor placement for these labels. Sponsored by City of Uster
1 parent 86ad564 commit 03f9e1b

13 files changed

+431
-4
lines changed

src/core/pal/feature.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ namespace pal
154154
return mLF->id();
155155
}
156156

157+
bool FeaturePart::hasSameLabelFeatureAs( FeaturePart* part ) const
158+
{
159+
if ( !part )
160+
return false;
161+
162+
return mLF->id() == part->featureId() && mLF->layer()->name() == part->layer()->name();
163+
}
164+
157165
LabelPosition::Quadrant FeaturePart::quadrantFromOffset() const
158166
{
159167
QPointF quadOffset = mLF->quadOffset();

src/core/pal/feature.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ namespace pal
165165
*/
166166
QgsFeatureId featureId() const;
167167

168+
/** Tests whether this feature part belongs to the same QgsLabelFeature as another
169+
* feature part.
170+
* @param part part to compare to
171+
* @returns true if both parts belong to same QgsLabelFeature
172+
*/
173+
bool hasSameLabelFeatureAs( FeaturePart* part ) const;
168174

169175
#if 0
170176
/**

src/core/pal/labelposition.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,16 +441,22 @@ namespace pal
441441

442442
//////////
443443

444-
bool LabelPosition::pruneCallback( LabelPosition *lp, void *ctx )
444+
bool LabelPosition::pruneCallback( LabelPosition *candidatePosition, void *ctx )
445445
{
446-
FeaturePart *feat = (( PruneCtx* ) ctx )->obstacle;
446+
FeaturePart *obstaclePart = (( PruneCtx* ) ctx )->obstacle;
447447

448-
if (( feat == lp->feature ) || ( feat->getHoleOf() && feat->getHoleOf() != lp->feature ) )
448+
// test whether we should ignore this obstacle for the candidate. We do this if:
449+
// 1. it's not a hole, and the obstacle belongs to the same label feature as the candidate (eg
450+
// features aren't obstacles for their own labels)
451+
// 2. it IS a hole, and the hole belongs to a different label feature to the candidate (eg, holes
452+
// are ONLY obstacles for the labels of the feature they belong to)
453+
if (( !obstaclePart->getHoleOf() && candidatePosition->feature->hasSameLabelFeatureAs( obstaclePart ) )
454+
|| ( obstaclePart->getHoleOf() && !candidatePosition->feature->hasSameLabelFeatureAs( dynamic_cast< FeaturePart* >( obstaclePart->getHoleOf() ) ) ) )
449455
{
450456
return true;
451457
}
452458

453-
CostCalculator::addObstacleCostPenalty( lp, feat );
459+
CostCalculator::addObstacleCostPenalty( candidatePosition, obstaclePart );
454460

455461
return true;
456462
}

tests/src/python/test_qgspallabeling_placement.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,40 @@ def test_point_placement_around_obstacle_large_symbol(self):
142142
self.removeMapLayer(self.layer)
143143
self.layer = None
144144

145+
def test_polygon_placement_with_hole(self):
146+
# Horizontal label placement for polygon with hole
147+
# Note for this test, the mask is used to check only pixels outside of the polygon.
148+
# We don't care where in the polygon the label is, just that it
149+
# is INSIDE the polygon
150+
self.layer = TestQgsPalLabeling.loadFeatureLayer('polygon_with_hole')
151+
self._TestMapSettings = self.cloneMapSettings(self._MapSettings)
152+
self.lyr.placement = QgsPalLayerSettings.Horizontal
153+
self.checkTest()
154+
self.removeMapLayer(self.layer)
155+
self.layer = None
156+
157+
def test_polygon_placement_with_hole_and_point(self):
158+
# Testing that hole from a feature is not treated as an obstacle for other feature's labels
159+
self.layer = TestQgsPalLabeling.loadFeatureLayer('point')
160+
polyLayer = TestQgsPalLabeling.loadFeatureLayer('polygon_with_hole')
161+
self._TestMapSettings = self.cloneMapSettings(self._MapSettings)
162+
self.checkTest()
163+
self.removeMapLayer(self.layer)
164+
self.removeMapLayer(polyLayer)
165+
self.layer = None
166+
167+
def test_polygon_multiple_labels(self):
168+
# Horizontal label placement for polygon with hole
169+
# Note for this test, the mask is used to check only pixels outside of the polygon.
170+
# We don't care where in the polygon the label is, just that it
171+
# is INSIDE the polygon
172+
self.layer = TestQgsPalLabeling.loadFeatureLayer('polygon_rule_based')
173+
self._TestMapSettings = self.cloneMapSettings(self._MapSettings)
174+
self.lyr.placement = QgsPalLayerSettings.Horizontal
175+
self.checkTest()
176+
self.removeMapLayer(self.layer)
177+
self.layer = None
178+
145179
if __name__ == '__main__':
146180
# NOTE: unless PAL_SUITE env var is set all test class methods will be run
147181
# SEE: test_qgspallabeling_tests.suiteTests() to define suite
Binary file not shown.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
2+
<qgis version="2.13.0-Master" minimumScale="0" maximumScale="1e+08" simplifyDrawingHints="1" minLabelScale="0" maxLabelScale="1e+08" simplifyDrawingTol="1" simplifyMaxScale="1" hasScaleBasedVisibilityFlag="0" simplifyLocal="1" scaleBasedLabelVisibilityFlag="0">
3+
<edittypes>
4+
<edittype widgetv2type="TextEdit" name="pkuid">
5+
<widgetv2config IsMultiline="0" fieldEditable="1" UseHtml="0" labelOnTop="0"/>
6+
</edittype>
7+
<edittype widgetv2type="TextEdit" name="ftype">
8+
<widgetv2config IsMultiline="0" fieldEditable="1" UseHtml="0" labelOnTop="0"/>
9+
</edittype>
10+
<edittype widgetv2type="TextEdit" name="text">
11+
<widgetv2config IsMultiline="0" fieldEditable="1" UseHtml="0" labelOnTop="0"/>
12+
</edittype>
13+
</edittypes>
14+
<renderer-v2 forceraster="0" symbollevels="0" type="singleSymbol">
15+
<symbols>
16+
<symbol alpha="1" clip_to_extent="1" type="fill" name="0">
17+
<layer pass="0" class="SimpleFill" locked="0">
18+
<prop k="border_width_map_unit_scale" v="0,0,0,0,0,0"/>
19+
<prop k="color" v="133,149,161,255"/>
20+
<prop k="joinstyle" v="bevel"/>
21+
<prop k="offset" v="0,0"/>
22+
<prop k="offset_map_unit_scale" v="0,0,0,0,0,0"/>
23+
<prop k="offset_unit" v="MM"/>
24+
<prop k="outline_color" v="0,0,0,255"/>
25+
<prop k="outline_style" v="no"/>
26+
<prop k="outline_width" v="0.26"/>
27+
<prop k="outline_width_unit" v="MM"/>
28+
<prop k="style" v="solid"/>
29+
</layer>
30+
</symbol>
31+
</symbols>
32+
<rotation/>
33+
<sizescale scalemethod="diameter"/>
34+
</renderer-v2>
35+
<labeling type="rule-based">
36+
<rules>
37+
<rule>
38+
<settings>
39+
<text-style fontItalic="1" fontFamily="Ubuntu" fontLetterSpacing="0" fontUnderline="0" fontSizeMapUnitMaxScale="0" fontWeight="63" fontStrikeout="0" textTransp="0" previewBkgrdColor="#ffffff" fontCapitals="0" textColor="0,0,0,255" fontSizeMapUnitMinScale="0" fontSizeInMapUnits="0" isExpression="0" blendMode="0" fontSize="11" fieldName="text" namedStyle="Medium Italic" fontWordSpacing="0"/>
40+
<text-format placeDirectionSymbol="0" multilineAlign="0" rightDirectionSymbol=">" multilineHeight="1" plussign="0" addDirectionSymbol="0" leftDirectionSymbol="&lt;" formatNumbers="0" decimals="3" wrapChar="" reverseDirectionSymbol="0"/>
41+
<text-buffer bufferSize="1" bufferSizeMapUnitMinScale="0" bufferColor="255,255,255,255" bufferDraw="0" bufferBlendMode="0" bufferTransp="0" bufferSizeInMapUnits="0" bufferSizeMapUnitMaxScale="0" bufferNoFill="0" bufferJoinStyle="64"/>
42+
<background shapeSizeUnits="1" shapeType="0" shapeOffsetMapUnitMinScale="0" shapeSizeMapUnitMinScale="0" shapeSVGFile="" shapeOffsetX="0" shapeOffsetY="0" shapeBlendMode="0" shapeBorderWidthMapUnitMaxScale="0" shapeFillColor="255,255,255,255" shapeTransparency="0" shapeSizeType="0" shapeJoinStyle="64" shapeDraw="0" shapeSizeMapUnitMaxScale="0" shapeBorderWidthUnits="1" shapeSizeX="0" shapeSizeY="0" shapeRadiiX="0" shapeOffsetMapUnitMaxScale="0" shapeOffsetUnits="1" shapeRadiiY="0" shapeRotation="0" shapeBorderWidth="0" shapeRadiiMapUnitMinScale="0" shapeRadiiMapUnitMaxScale="0" shapeBorderColor="128,128,128,255" shapeRotationType="0" shapeRadiiUnits="1" shapeBorderWidthMapUnitMinScale="0"/>
43+
<shadow shadowOffsetGlobal="1" shadowRadiusUnits="1" shadowRadiusMapUnitMinScale="0" shadowTransparency="30" shadowColor="0,0,0,255" shadowUnder="0" shadowScale="100" shadowOffsetDist="1" shadowOffsetMapUnitMinScale="0" shadowRadiusMapUnitMaxScale="0" shadowDraw="0" shadowOffsetAngle="135" shadowRadius="1.5" shadowBlendMode="6" shadowOffsetMapUnitMaxScale="0" shadowRadiusAlphaOnly="0" shadowOffsetUnits="1"/>
44+
<placement repeatDistanceUnit="1" placement="1" maxCurvedCharAngleIn="20" repeatDistance="0" distMapUnitMaxScale="0" labelOffsetMapUnitMaxScale="0" distInMapUnits="0" labelOffsetInMapUnits="0" xOffset="0" preserveRotation="1" centroidWhole="0" priority="5" repeatDistanceMapUnitMaxScale="0" yOffset="-2" placementFlags="10" repeatDistanceMapUnitMinScale="0" centroidInside="0" dist="0" angleOffset="0" maxCurvedCharAngleOut="-20" fitInPolygonOnly="0" quadOffset="1" distMapUnitMinScale="0" labelOffsetMapUnitMinScale="0"/>
45+
<rendering fontMinPixelSize="3" scaleMax="10000000" fontMaxPixelSize="10000" scaleMin="1" upsidedownLabels="0" limitNumLabels="0" obstacle="1" obstacleFactor="1" scaleVisibility="0" fontLimitPixelSize="0" mergeLines="0" obstacleType="0" labelPerPart="0" maxNumLabels="2000" displayAll="0" minFeatureSize="0"/>
46+
<data-defined/>
47+
</settings>
48+
</rule>
49+
<rule>
50+
<settings>
51+
<text-style fontItalic="1" fontFamily="Ubuntu" fontLetterSpacing="0" fontUnderline="0" fontSizeMapUnitMaxScale="0" fontWeight="63" fontStrikeout="0" textTransp="0" previewBkgrdColor="#ffffff" fontCapitals="0" textColor="0,0,0,255" fontSizeMapUnitMinScale="0" fontSizeInMapUnits="0" isExpression="0" blendMode="0" fontSize="11" fieldName="text" namedStyle="Medium Italic" fontWordSpacing="0"/>
52+
<text-format placeDirectionSymbol="0" multilineAlign="0" rightDirectionSymbol=">" multilineHeight="1" plussign="0" addDirectionSymbol="0" leftDirectionSymbol="&lt;" formatNumbers="0" decimals="3" wrapChar="" reverseDirectionSymbol="0"/>
53+
<text-buffer bufferSize="1" bufferSizeMapUnitMinScale="0" bufferColor="255,255,255,255" bufferDraw="0" bufferBlendMode="0" bufferTransp="0" bufferSizeInMapUnits="0" bufferSizeMapUnitMaxScale="0" bufferNoFill="0" bufferJoinStyle="64"/>
54+
<background shapeSizeUnits="1" shapeType="0" shapeOffsetMapUnitMinScale="0" shapeSizeMapUnitMinScale="0" shapeSVGFile="" shapeOffsetX="0" shapeOffsetY="0" shapeBlendMode="0" shapeBorderWidthMapUnitMaxScale="0" shapeFillColor="255,255,255,255" shapeTransparency="0" shapeSizeType="0" shapeJoinStyle="64" shapeDraw="0" shapeSizeMapUnitMaxScale="0" shapeBorderWidthUnits="1" shapeSizeX="0" shapeSizeY="0" shapeRadiiX="0" shapeOffsetMapUnitMaxScale="0" shapeOffsetUnits="1" shapeRadiiY="0" shapeRotation="0" shapeBorderWidth="0" shapeRadiiMapUnitMinScale="0" shapeRadiiMapUnitMaxScale="0" shapeBorderColor="128,128,128,255" shapeRotationType="0" shapeRadiiUnits="1" shapeBorderWidthMapUnitMinScale="0"/>
55+
<shadow shadowOffsetGlobal="1" shadowRadiusUnits="1" shadowRadiusMapUnitMinScale="0" shadowTransparency="30" shadowColor="0,0,0,255" shadowUnder="0" shadowScale="100" shadowOffsetDist="1" shadowOffsetMapUnitMinScale="0" shadowRadiusMapUnitMaxScale="0" shadowDraw="0" shadowOffsetAngle="135" shadowRadius="1.5" shadowBlendMode="6" shadowOffsetMapUnitMaxScale="0" shadowRadiusAlphaOnly="0" shadowOffsetUnits="1"/>
56+
<placement repeatDistanceUnit="1" placement="1" maxCurvedCharAngleIn="20" repeatDistance="0" distMapUnitMaxScale="0" labelOffsetMapUnitMaxScale="0" distInMapUnits="0" labelOffsetInMapUnits="0" xOffset="0" preserveRotation="1" centroidWhole="0" priority="5" repeatDistanceMapUnitMaxScale="0" yOffset="2" placementFlags="10" repeatDistanceMapUnitMinScale="0" centroidInside="0" dist="0" angleOffset="0" maxCurvedCharAngleOut="-20" fitInPolygonOnly="0" quadOffset="7" distMapUnitMinScale="0" labelOffsetMapUnitMinScale="0"/>
57+
<rendering fontMinPixelSize="3" scaleMax="10000000" fontMaxPixelSize="10000" scaleMin="1" upsidedownLabels="0" limitNumLabels="0" obstacle="1" obstacleFactor="1" scaleVisibility="0" fontLimitPixelSize="0" mergeLines="0" obstacleType="0" labelPerPart="0" maxNumLabels="2000" displayAll="0" minFeatureSize="0"/>
58+
<data-defined/>
59+
</settings>
60+
</rule>
61+
</rules>
62+
</labeling>
63+
<customproperties>
64+
<property key="variableNames" value="_fields_"/>
65+
<property key="variableValues" value=""/>
66+
</customproperties>
67+
<blendMode>0</blendMode>
68+
<featureBlendMode>0</featureBlendMode>
69+
<layerTransparency>0</layerTransparency>
70+
<displayfield>pkuid</displayfield>
71+
<label>0</label>
72+
<labelattributes>
73+
<label fieldname="" text="Label"/>
74+
<family fieldname="" name="Ubuntu"/>
75+
<size fieldname="" units="pt" value="12"/>
76+
<bold fieldname="" on="0"/>
77+
<italic fieldname="" on="0"/>
78+
<underline fieldname="" on="0"/>
79+
<strikeout fieldname="" on="0"/>
80+
<color fieldname="" red="0" blue="0" green="0"/>
81+
<x fieldname=""/>
82+
<y fieldname=""/>
83+
<offset x="0" y="0" units="pt" yfieldname="" xfieldname=""/>
84+
<angle fieldname="" value="0" auto="0"/>
85+
<alignment fieldname="" value="center"/>
86+
<buffercolor fieldname="" red="255" blue="255" green="255"/>
87+
<buffersize fieldname="" units="pt" value="1"/>
88+
<bufferenabled fieldname="" on=""/>
89+
<multilineenabled fieldname="" on=""/>
90+
<selectedonly on=""/>
91+
</labelattributes>
92+
<SingleCategoryDiagramRenderer diagramType="Pie">
93+
<DiagramCategory penColor="#000000" labelPlacementMethod="XHeight" penWidth="0" diagramOrientation="Up" minimumSize="0" barWidth="5" penAlpha="255" maxScaleDenominator="1e+08" backgroundColor="#ffffff" transparency="0" width="15" scaleDependency="Area" backgroundAlpha="255" angleOffset="1440" scaleBasedVisibility="0" enabled="0" height="15" sizeType="MM" minScaleDenominator="0">
94+
<fontProperties description="Ubuntu,11,-1,5,50,0,0,0,0,0" style=""/>
95+
</DiagramCategory>
96+
</SingleCategoryDiagramRenderer>
97+
<DiagramLayerSettings yPosColumn="-1" linePlacementFlags="10" placement="0" dist="0" xPosColumn="-1" priority="0" obstacle="0" showAll="1"/>
98+
<editform></editform>
99+
<editforminit/>
100+
<editforminitusecode>0</editforminitusecode>
101+
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
102+
"""
103+
QGIS forms can have a Python function that is called when the form is
104+
opened.
105+
106+
Use this function to add extra logic to your forms.
107+
108+
Enter the name of the function in the "Python Init function"
109+
field.
110+
An example follows:
111+
"""
112+
from PyQt4.QtGui import QWidget
113+
114+
def my_form_open(dialog, layer, feature):
115+
geom = feature.geometry()
116+
control = dialog.findChild(QWidget, "MyLineEdit")
117+
]]></editforminitcode>
118+
<featformsuppress>0</featformsuppress>
119+
<annotationform></annotationform>
120+
<editorlayout>generatedlayout</editorlayout>
121+
<excludeAttributesWMS/>
122+
<excludeAttributesWFS/>
123+
<attributeactions/>
124+
<conditionalstyles>
125+
<rowstyles/>
126+
<fieldstyles/>
127+
</conditionalstyles>
128+
</qgis>

0 commit comments

Comments
 (0)