Skip to content

Commit 35855b8

Browse files
committedDec 15, 2018
[FEATURE][API] Add API to set a margin for labels for layout map items
This controls how close labels are permitted to the edges of the map item. The labeling engine will then try other candidate positions in order to avoid placing labels within this margin.
1 parent 4252aab commit 35855b8

File tree

10 files changed

+161
-2
lines changed

10 files changed

+161
-2
lines changed
 

‎python/core/auto_generated/layout/qgslayoutitem.sip.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ Base class for graphical items within a :py:class:`QgsLayout`.
187187
UndoMapGridAnnotationFontColor,
188188
UndoMapGridLineSymbol,
189189
UndoMapGridMarkerSymbol,
190+
UndoMapLabelMargin,
190191
UndoPictureRotation,
191192
UndoPictureFillColor,
192193
UndoPictureStrokeColor,

‎python/core/auto_generated/layout/qgslayoutitemmap.sip.in

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,30 @@ Returns the map item's first overview. This is a convenience function.
448448
:return: pointer to first overview for map item
449449

450450
.. seealso:: :py:func:`overviews`
451+
%End
452+
453+
QgsLayoutMeasurement labelMargin() const;
454+
%Docstring
455+
Returns the margin from the map edges in which no labels may be placed.
456+
457+
If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
458+
of the map.
459+
460+
.. seealso:: :py:func:`setLabelMargin`
461+
462+
.. versionadded:: 3.6
463+
%End
464+
465+
void setLabelMargin( const QgsLayoutMeasurement &margin );
466+
%Docstring
467+
Sets the ``margin`` from the map edges in which no labels may be placed.
468+
469+
If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
470+
of the map.
471+
472+
.. seealso:: :py:func:`labelMargin`
473+
474+
.. versionadded:: 3.6
451475
%End
452476

453477
virtual QgsExpressionContext createExpressionContext() const;

‎src/core/layout/qgslayoutitem.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
236236
UndoMapGridAnnotationFontColor, //!< Map frame annotation color
237237
UndoMapGridLineSymbol, //!< Grid line symbol
238238
UndoMapGridMarkerSymbol, //!< Grid marker symbol
239+
UndoMapLabelMargin, //!< Margin for labels from edge of map
239240
UndoPictureRotation, //!< Picture rotation
240241
UndoPictureFillColor, //!< Picture fill color
241242
UndoPictureStrokeColor, //!< Picture stroke color

‎src/core/layout/qgslayoutitemmap.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,8 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum
607607
atlasElem.setAttribute( QStringLiteral( "margin" ), qgsDoubleToString( mAtlasMargin ) );
608608
mapElem.appendChild( atlasElem );
609609

610+
mapElem.setAttribute( QStringLiteral( "labelMargin" ), mLabelMargin.encodeMeasurement() );
611+
610612
return true;
611613
}
612614

@@ -740,6 +742,8 @@ bool QgsLayoutItemMap::readPropertiesFromElement( const QDomElement &itemElem, c
740742
mAtlasMargin = atlasElem.attribute( QStringLiteral( "margin" ), QStringLiteral( "0.1" ) ).toDouble();
741743
}
742744

745+
mLabelMargin = QgsLayoutMeasurement::decodeMeasurement( itemElem.attribute( QStringLiteral( "labelMargin" ), QStringLiteral( "0" ) ) );
746+
743747
updateBoundingRect();
744748

745749
mUpdatesEnabled = true;
@@ -1127,6 +1131,17 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF
11271131
// override the default text render format inherited from the labeling engine settings using the layout's render context setting
11281132
jobMapSettings.setTextRenderFormat( mLayout->renderContext().textRenderFormat() );
11291133

1134+
if ( mLabelMargin.length() > 0 )
1135+
{
1136+
QPolygonF visiblePoly = jobMapSettings.visiblePolygon();
1137+
visiblePoly.append( visiblePoly.at( 0 ) ); //close polygon
1138+
const double layoutLabelMargin = mLayout->convertToLayoutUnits( mLabelMargin );
1139+
const double layoutLabelMarginInMapUnits = layoutLabelMargin / rect().width() * jobMapSettings.extent().width();
1140+
QgsGeometry mapBoundaryGeom = QgsGeometry::fromQPolygonF( visiblePoly );
1141+
mapBoundaryGeom = mapBoundaryGeom.buffer( -layoutLabelMarginInMapUnits, 0 );
1142+
jobMapSettings.setLabelBoundaryGeometry( mapBoundaryGeom );
1143+
}
1144+
11301145
return jobMapSettings;
11311146
}
11321147

@@ -1413,6 +1428,16 @@ void QgsLayoutItemMap::connectUpdateSlot()
14131428
connect( project->mapThemeCollection(), &QgsMapThemeCollection::mapThemeChanged, this, &QgsLayoutItemMap::mapThemeChanged );
14141429
}
14151430

1431+
QgsLayoutMeasurement QgsLayoutItemMap::labelMargin() const
1432+
{
1433+
return mLabelMargin;
1434+
}
1435+
1436+
void QgsLayoutItemMap::setLabelMargin( const QgsLayoutMeasurement &margin )
1437+
{
1438+
mLabelMargin = margin;
1439+
}
1440+
14161441
void QgsLayoutItemMap::updateToolTip()
14171442
{
14181443
setToolTip( displayName() );

‎src/core/layout/qgslayoutitemmap.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,30 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem
398398
*/
399399
QgsLayoutItemMapOverview *overview();
400400

401+
/**
402+
* Returns the margin from the map edges in which no labels may be placed.
403+
*
404+
* If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
405+
* of the map.
406+
*
407+
* \see setLabelMargin()
408+
*
409+
* \since QGIS 3.6
410+
*/
411+
QgsLayoutMeasurement labelMargin() const;
412+
413+
/**
414+
* Sets the \a margin from the map edges in which no labels may be placed.
415+
*
416+
* If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
417+
* of the map.
418+
*
419+
* \see labelMargin()
420+
*
421+
* \since QGIS 3.6
422+
*/
423+
void setLabelMargin( const QgsLayoutMeasurement &margin );
424+
401425
QgsExpressionContext createExpressionContext() const override;
402426

403427
/**
@@ -621,6 +645,8 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem
621645
std::unique_ptr< QgsMapRendererCustomPainterJob > mPainterJob;
622646
bool mPainterCancelWait = false;
623647

648+
QgsLayoutMeasurement mLabelMargin{ 0 };
649+
624650
void init();
625651

626652
//! Resets the item tooltip to reflect current map id

‎tests/src/python/test_qgslayoutmap.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,17 @@
2828
QgsMapSettings,
2929
QgsProject,
3030
QgsMultiBandColorRenderer,
31-
QgsCoordinateReferenceSystem
32-
)
31+
QgsCoordinateReferenceSystem,
32+
QgsTextFormat,
33+
QgsFontUtils,
34+
QgsPalLayerSettings,
35+
QgsNullSymbolRenderer,
36+
QgsPoint,
37+
QgsFeature,
38+
QgsVectorLayerSimpleLabeling,
39+
QgsLabelingEngineSettings,
40+
QgsLayoutMeasurement,
41+
QgsUnitTypes)
3342

3443
from qgis.testing import start_app, unittest
3544
from utilities import unitTestDataPath
@@ -244,6 +253,79 @@ def testRasterization(self):
244253

245254
self.vector_layer.setBlendMode(QPainter.CompositionMode_SourceOver)
246255

256+
def testLabelMargin(self):
257+
"""
258+
Test rendering map item with a label margin set
259+
"""
260+
format = QgsTextFormat()
261+
format.setFont(QgsFontUtils.getStandardTestFont("Bold"))
262+
format.setSize(20)
263+
format.setNamedStyle("Bold")
264+
format.setColor(QColor(0, 0, 0))
265+
settings = QgsPalLayerSettings()
266+
settings.setFormat(format)
267+
settings.fieldName = "'X'"
268+
settings.isExpression = True
269+
settings.placement = QgsPalLayerSettings.OverPoint
270+
271+
vl = QgsVectorLayer("Point?crs=epsg:4326&field=id:integer", "vl", "memory")
272+
vl.setRenderer(QgsNullSymbolRenderer())
273+
f = QgsFeature(vl.fields(), 1)
274+
for x in range(15):
275+
for y in range(15):
276+
f.setGeometry(QgsPoint(x, y))
277+
vl.dataProvider().addFeature(f)
278+
279+
vl.setLabeling(QgsVectorLayerSimpleLabeling(settings))
280+
vl.setLabelsEnabled(True)
281+
282+
p = QgsProject()
283+
284+
engine_settings = QgsLabelingEngineSettings()
285+
engine_settings.setFlag(QgsLabelingEngineSettings.UsePartialCandidates, False)
286+
engine_settings.setFlag(QgsLabelingEngineSettings.DrawLabelRectOnly, True)
287+
p.setLabelingEngineSettings(engine_settings)
288+
289+
p.addMapLayer(vl)
290+
layout = QgsLayout(p)
291+
layout.initializeDefaults()
292+
p.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
293+
map = QgsLayoutItemMap(layout)
294+
map.attemptSetSceneRect(QRectF(10, 10, 180, 180))
295+
map.setFrameEnabled(True)
296+
map.zoomToExtent(vl.extent())
297+
map.setLayers([vl])
298+
layout.addLayoutItem(map)
299+
300+
checker = QgsLayoutChecker('composermap_label_nomargin', layout)
301+
checker.setControlPathPrefix("composer_map")
302+
result, message = checker.testLayout()
303+
self.report += checker.report()
304+
self.assertTrue(result, message)
305+
306+
map.setLabelMargin(QgsLayoutMeasurement(15, QgsUnitTypes.LayoutMillimeters))
307+
checker = QgsLayoutChecker('composermap_label_margin', layout)
308+
checker.setControlPathPrefix("composer_map")
309+
result, message = checker.testLayout()
310+
self.report += checker.report()
311+
self.assertTrue(result, message)
312+
313+
map.setLabelMargin(QgsLayoutMeasurement(3, QgsUnitTypes.LayoutCentimeters))
314+
checker = QgsLayoutChecker('composermap_label_cm_margin', layout)
315+
checker.setControlPathPrefix("composer_map")
316+
result, message = checker.testLayout()
317+
self.report += checker.report()
318+
self.assertTrue(result, message)
319+
320+
map.setMapRotation(45)
321+
map.zoomToExtent(vl.extent())
322+
map.setScale(map.scale() * 1.2)
323+
checker = QgsLayoutChecker('composermap_rotated_label_margin', layout)
324+
checker.setControlPathPrefix("composer_map")
325+
result, message = checker.testLayout()
326+
self.report += checker.report()
327+
self.assertTrue(result, message)
328+
247329

248330
if __name__ == '__main__':
249331
unittest.main()

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

Error rendering embedded code

Invalid image source.

0 commit comments

Comments
 (0)
Please sign in to comment.