Skip to content

Commit

Permalink
[FEATURE][API] Add API to set a margin for labels for layout map items
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nyalldawson committed Dec 15, 2018
1 parent 4252aab commit 35855b8
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 2 deletions.
1 change: 1 addition & 0 deletions python/core/auto_generated/layout/qgslayoutitem.sip.in
Expand Up @@ -187,6 +187,7 @@ Base class for graphical items within a :py:class:`QgsLayout`.
UndoMapGridAnnotationFontColor,
UndoMapGridLineSymbol,
UndoMapGridMarkerSymbol,
UndoMapLabelMargin,
UndoPictureRotation,
UndoPictureFillColor,
UndoPictureStrokeColor,
Expand Down
24 changes: 24 additions & 0 deletions python/core/auto_generated/layout/qgslayoutitemmap.sip.in
Expand Up @@ -448,6 +448,30 @@ Returns the map item's first overview. This is a convenience function.
:return: pointer to first overview for map item

.. seealso:: :py:func:`overviews`
%End

QgsLayoutMeasurement labelMargin() const;
%Docstring
Returns the margin from the map edges in which no labels may be placed.

If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
of the map.

.. seealso:: :py:func:`setLabelMargin`

.. versionadded:: 3.6
%End

void setLabelMargin( const QgsLayoutMeasurement &margin );
%Docstring
Sets the ``margin`` from the map edges in which no labels may be placed.

If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
of the map.

.. seealso:: :py:func:`labelMargin`

.. versionadded:: 3.6
%End

virtual QgsExpressionContext createExpressionContext() const;
Expand Down
1 change: 1 addition & 0 deletions src/core/layout/qgslayoutitem.h
Expand Up @@ -236,6 +236,7 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
UndoMapGridAnnotationFontColor, //!< Map frame annotation color
UndoMapGridLineSymbol, //!< Grid line symbol
UndoMapGridMarkerSymbol, //!< Grid marker symbol
UndoMapLabelMargin, //!< Margin for labels from edge of map
UndoPictureRotation, //!< Picture rotation
UndoPictureFillColor, //!< Picture fill color
UndoPictureStrokeColor, //!< Picture stroke color
Expand Down
25 changes: 25 additions & 0 deletions src/core/layout/qgslayoutitemmap.cpp
Expand Up @@ -607,6 +607,8 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum
atlasElem.setAttribute( QStringLiteral( "margin" ), qgsDoubleToString( mAtlasMargin ) );
mapElem.appendChild( atlasElem );

mapElem.setAttribute( QStringLiteral( "labelMargin" ), mLabelMargin.encodeMeasurement() );

return true;
}

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

mLabelMargin = QgsLayoutMeasurement::decodeMeasurement( itemElem.attribute( QStringLiteral( "labelMargin" ), QStringLiteral( "0" ) ) );

updateBoundingRect();

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

if ( mLabelMargin.length() > 0 )
{
QPolygonF visiblePoly = jobMapSettings.visiblePolygon();
visiblePoly.append( visiblePoly.at( 0 ) ); //close polygon
const double layoutLabelMargin = mLayout->convertToLayoutUnits( mLabelMargin );
const double layoutLabelMarginInMapUnits = layoutLabelMargin / rect().width() * jobMapSettings.extent().width();
QgsGeometry mapBoundaryGeom = QgsGeometry::fromQPolygonF( visiblePoly );
mapBoundaryGeom = mapBoundaryGeom.buffer( -layoutLabelMarginInMapUnits, 0 );
jobMapSettings.setLabelBoundaryGeometry( mapBoundaryGeom );
}

return jobMapSettings;
}

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

QgsLayoutMeasurement QgsLayoutItemMap::labelMargin() const
{
return mLabelMargin;
}

void QgsLayoutItemMap::setLabelMargin( const QgsLayoutMeasurement &margin )
{
mLabelMargin = margin;
}

void QgsLayoutItemMap::updateToolTip()
{
setToolTip( displayName() );
Expand Down
26 changes: 26 additions & 0 deletions src/core/layout/qgslayoutitemmap.h
Expand Up @@ -398,6 +398,30 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem
*/
QgsLayoutItemMapOverview *overview();

/**
* Returns the margin from the map edges in which no labels may be placed.
*
* If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
* of the map.
*
* \see setLabelMargin()
*
* \since QGIS 3.6
*/
QgsLayoutMeasurement labelMargin() const;

/**
* Sets the \a margin from the map edges in which no labels may be placed.
*
* If the margin is 0 then labels can be placed right up to the edge (and possibly overlapping the edge)
* of the map.
*
* \see labelMargin()
*
* \since QGIS 3.6
*/
void setLabelMargin( const QgsLayoutMeasurement &margin );

QgsExpressionContext createExpressionContext() const override;

/**
Expand Down Expand Up @@ -621,6 +645,8 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem
std::unique_ptr< QgsMapRendererCustomPainterJob > mPainterJob;
bool mPainterCancelWait = false;

QgsLayoutMeasurement mLabelMargin{ 0 };

void init();

//! Resets the item tooltip to reflect current map id
Expand Down
86 changes: 84 additions & 2 deletions tests/src/python/test_qgslayoutmap.py
Expand Up @@ -28,8 +28,17 @@
QgsMapSettings,
QgsProject,
QgsMultiBandColorRenderer,
QgsCoordinateReferenceSystem
)
QgsCoordinateReferenceSystem,
QgsTextFormat,
QgsFontUtils,
QgsPalLayerSettings,
QgsNullSymbolRenderer,
QgsPoint,
QgsFeature,
QgsVectorLayerSimpleLabeling,
QgsLabelingEngineSettings,
QgsLayoutMeasurement,
QgsUnitTypes)

from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
Expand Down Expand Up @@ -244,6 +253,79 @@ def testRasterization(self):

self.vector_layer.setBlendMode(QPainter.CompositionMode_SourceOver)

def testLabelMargin(self):
"""
Test rendering map item with a label margin set
"""
format = QgsTextFormat()
format.setFont(QgsFontUtils.getStandardTestFont("Bold"))
format.setSize(20)
format.setNamedStyle("Bold")
format.setColor(QColor(0, 0, 0))
settings = QgsPalLayerSettings()
settings.setFormat(format)
settings.fieldName = "'X'"
settings.isExpression = True
settings.placement = QgsPalLayerSettings.OverPoint

vl = QgsVectorLayer("Point?crs=epsg:4326&field=id:integer", "vl", "memory")
vl.setRenderer(QgsNullSymbolRenderer())
f = QgsFeature(vl.fields(), 1)
for x in range(15):
for y in range(15):
f.setGeometry(QgsPoint(x, y))
vl.dataProvider().addFeature(f)

vl.setLabeling(QgsVectorLayerSimpleLabeling(settings))
vl.setLabelsEnabled(True)

p = QgsProject()

engine_settings = QgsLabelingEngineSettings()
engine_settings.setFlag(QgsLabelingEngineSettings.UsePartialCandidates, False)
engine_settings.setFlag(QgsLabelingEngineSettings.DrawLabelRectOnly, True)
p.setLabelingEngineSettings(engine_settings)

p.addMapLayer(vl)
layout = QgsLayout(p)
layout.initializeDefaults()
p.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
map = QgsLayoutItemMap(layout)
map.attemptSetSceneRect(QRectF(10, 10, 180, 180))
map.setFrameEnabled(True)
map.zoomToExtent(vl.extent())
map.setLayers([vl])
layout.addLayoutItem(map)

checker = QgsLayoutChecker('composermap_label_nomargin', layout)
checker.setControlPathPrefix("composer_map")
result, message = checker.testLayout()
self.report += checker.report()
self.assertTrue(result, message)

map.setLabelMargin(QgsLayoutMeasurement(15, QgsUnitTypes.LayoutMillimeters))
checker = QgsLayoutChecker('composermap_label_margin', layout)
checker.setControlPathPrefix("composer_map")
result, message = checker.testLayout()
self.report += checker.report()
self.assertTrue(result, message)

map.setLabelMargin(QgsLayoutMeasurement(3, QgsUnitTypes.LayoutCentimeters))
checker = QgsLayoutChecker('composermap_label_cm_margin', layout)
checker.setControlPathPrefix("composer_map")
result, message = checker.testLayout()
self.report += checker.report()
self.assertTrue(result, message)

map.setMapRotation(45)
map.zoomToExtent(vl.extent())
map.setScale(map.scale() * 1.2)
checker = QgsLayoutChecker('composermap_rotated_label_margin', layout)
checker.setControlPathPrefix("composer_map")
result, message = checker.testLayout()
self.report += checker.report()
self.assertTrue(result, message)


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.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 35855b8

Please sign in to comment.