Skip to content

Commit

Permalink
[layouts] Add API to set item based clipping region on maps
Browse files Browse the repository at this point in the history
Allows using shape or polygon layout items to set the overall shape
of a layout map item's contents
  • Loading branch information
nyalldawson committed Jul 28, 2020
1 parent 915615a commit 264bd51
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 13 deletions.
8 changes: 4 additions & 4 deletions python/core/auto_generated/layout/qgslayoutitemmap.sip.in
Expand Up @@ -195,18 +195,18 @@ Sets the feature clipping ``type`` to apply when clipping to the associated item
.. seealso:: :py:func:`featureClippingType`
%End

bool forceLabelsInsideFeature() const;
bool forceLabelsInsideClipPath() const;
%Docstring
Returns ``True`` if labels should only be placed inside the clip path geometry.

.. seealso:: :py:func:`setForceLabelsInsideFeature`
.. seealso:: :py:func:`setForceLabelsInsideClipPath`
%End

void setForceLabelsInsideFeature( bool forceInside );
void setForceLabelsInsideClipPath( bool forceInside );
%Docstring
Sets whether labels should only be placed inside the clip path geometry.

.. seealso:: :py:func:`forceLabelsInsideFeature`
.. seealso:: :py:func:`forceLabelsInsideClipPath`
%End

bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
Expand Down
6 changes: 3 additions & 3 deletions src/core/layout/qgslayoutitemmap.cpp
Expand Up @@ -1539,7 +1539,7 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF
{
jobMapSettings.addClippingRegion( mItemClippingSettings->toMapClippingRegion() );

if ( mItemClippingSettings->forceLabelsInsideFeature() )
if ( mItemClippingSettings->forceLabelsInsideClipPath() )
{
const double layoutLabelMargin = mLayout->convertToLayoutUnits( mEvaluatedLabelMargin );
const double layoutLabelMarginInMapUnits = layoutLabelMargin / rect().width() * jobMapSettings.extent().width();
Expand Down Expand Up @@ -2921,12 +2921,12 @@ void QgsLayoutItemMapItemClipPathSettings::setFeatureClippingType( QgsMapClippin
emit changed();
}

bool QgsLayoutItemMapItemClipPathSettings::forceLabelsInsideFeature() const
bool QgsLayoutItemMapItemClipPathSettings::forceLabelsInsideClipPath() const
{
return mForceLabelsInsideClipPath;
}

void QgsLayoutItemMapItemClipPathSettings::setForceLabelsInsideFeature( bool forceInside )
void QgsLayoutItemMapItemClipPathSettings::setForceLabelsInsideClipPath( bool forceInside )
{
if ( forceInside == mForceLabelsInsideClipPath )
return;
Expand Down
8 changes: 4 additions & 4 deletions src/core/layout/qgslayoutitemmap.h
Expand Up @@ -224,16 +224,16 @@ class CORE_EXPORT QgsLayoutItemMapItemClipPathSettings : public QObject
/**
* Returns TRUE if labels should only be placed inside the clip path geometry.
*
* \see setForceLabelsInsideFeature()
* \see setForceLabelsInsideClipPath()
*/
bool forceLabelsInsideFeature() const;
bool forceLabelsInsideClipPath() const;

/**
* Sets whether labels should only be placed inside the clip path geometry.
*
* \see forceLabelsInsideFeature()
* \see forceLabelsInsideClipPath()
*/
void setForceLabelsInsideFeature( bool forceInside );
void setForceLabelsInsideClipPath( bool forceInside );

/**
* Stores settings in a DOM element, where \a element is the DOM element
Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -126,6 +126,7 @@ ADD_PYTHON_TEST(PyQgsLayoutLabel test_qgslayoutlabel.py)
ADD_PYTHON_TEST(PyQgsLayoutLegend test_qgslayoutlegend.py)
ADD_PYTHON_TEST(PyQgsLayoutMap test_qgslayoutmap.py)
ADD_PYTHON_TEST(PyQgsLayoutItemMapAtlasClippingSettings test_qgslayoutatlasclippingsettings.py)
ADD_PYTHON_TEST(PyQgsLayoutItemMapItemClipPathSettings test_qgslayoutmapitemclippingsettings.py)
ADD_PYTHON_TEST(PyQgsLayoutMapGrid test_qgslayoutmapgrid.py)
ADD_PYTHON_TEST(PyQgsLayoutMapOverview test_qgslayoutmapoverview.py)
ADD_PYTHON_TEST(PyQgsLayoutMarker test_qgslayoutmarker.py)
Expand Down
129 changes: 127 additions & 2 deletions tests/src/python/test_qgslayoutmap.py
Expand Up @@ -14,7 +14,7 @@

import os

from qgis.PyQt.QtCore import QFileInfo, QRectF, QDir
from qgis.PyQt.QtCore import QFileInfo, QRectF, QDir, QCoreApplication, QEvent
from qgis.PyQt.QtXml import QDomDocument
from qgis.PyQt.QtGui import QPainter, QColor
from qgis.PyQt.QtTest import QSignalSpy
Expand All @@ -40,7 +40,12 @@
QgsUnitTypes,
QgsLayoutObject,
QgsProperty,
QgsReadWriteContext)
QgsReadWriteContext,
QgsFillSymbol,
QgsSingleSymbolRenderer,
QgsGeometry,
QgsLayoutItemShape,
QgsMapClippingRegion)

from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
Expand Down Expand Up @@ -456,6 +461,126 @@ def testTheme(self):
self.assertEqual(len(spy), 6)
self.assertEqual(spy[-1][0], 'theme6')

def testClipping(self):
format = QgsTextFormat()
format.setFont(QgsFontUtils.getStandardTestFont("Bold"))
format.setSize(30)
format.setNamedStyle("Bold")
format.setColor(QColor(0, 0, 0))
settings = QgsPalLayerSettings()
settings.setFormat(format)
settings.fieldName = "'XXXX'"
settings.isExpression = True
settings.placement = QgsPalLayerSettings.OverPoint

vl = QgsVectorLayer("Polygon?crs=epsg:4326&field=id:integer", "vl", "memory")

props = {"color": "127,255,127", 'outline_style': 'solid', 'outline_width': '1', 'outline_color': '0,0,255'}
fillSymbol = QgsFillSymbol.createSimple(props)
renderer = QgsSingleSymbolRenderer(fillSymbol)
vl.setRenderer(renderer)

f = QgsFeature(vl.fields(), 1)
for x in range(0, 15, 3):
for y in range(0, 15, 3):
f.setGeometry(QgsGeometry(QgsPoint(x, y)).buffer(1, 3))
vl.dataProvider().addFeature(f)

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

p = QgsProject()

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)

shape = QgsLayoutItemShape(layout)
layout.addLayoutItem(shape)
shape.setShapeType(QgsLayoutItemShape.Ellipse)
shape.attemptSetSceneRect(QRectF(10, 10, 180, 180))
props = {"color": "0,0,0,0", 'outline_style': 'no'}
fillSymbol = QgsFillSymbol.createSimple(props)
shape.setSymbol(fillSymbol)

map.itemClippingSettings().setEnabled(True)
map.itemClippingSettings().setSourceItem(shape)
map.itemClippingSettings().setForceLabelsInsideClipPath(False)
map.itemClippingSettings().setFeatureClippingType(QgsMapClippingRegion.FeatureClippingType.ClipToIntersection)

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

def testClippingForceLabelsInside(self):
format = QgsTextFormat()
format.setFont(QgsFontUtils.getStandardTestFont("Bold"))
format.setSize(30)
format.setNamedStyle("Bold")
format.setColor(QColor(0, 0, 0))
settings = QgsPalLayerSettings()
settings.setFormat(format)
settings.fieldName = "'XXXX'"
settings.isExpression = True
settings.placement = QgsPalLayerSettings.OverPoint

vl = QgsVectorLayer("Polygon?crs=epsg:4326&field=id:integer", "vl", "memory")

props = {"color": "127,255,127", 'outline_style': 'solid', 'outline_width': '1', 'outline_color': '0,0,255'}
fillSymbol = QgsFillSymbol.createSimple(props)
renderer = QgsSingleSymbolRenderer(fillSymbol)
vl.setRenderer(renderer)

f = QgsFeature(vl.fields(), 1)
for x in range(0, 15, 3):
for y in range(0, 15, 3):
f.setGeometry(QgsGeometry(QgsPoint(x, y)).buffer(1, 3))
vl.dataProvider().addFeature(f)

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

p = QgsProject()

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)

shape = QgsLayoutItemShape(layout)
layout.addLayoutItem(shape)
shape.setShapeType(QgsLayoutItemShape.Ellipse)
shape.attemptSetSceneRect(QRectF(10, 10, 180, 180))
props = {"color": "0,0,0,0", 'outline_style': 'no'}
fillSymbol = QgsFillSymbol.createSimple(props)
shape.setSymbol(fillSymbol)

map.itemClippingSettings().setEnabled(True)
map.itemClippingSettings().setSourceItem(shape)
map.itemClippingSettings().setForceLabelsInsideClipPath(True)
map.itemClippingSettings().setFeatureClippingType(QgsMapClippingRegion.FeatureClippingType.ClipPainterOnly)

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


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

0 comments on commit 264bd51

Please sign in to comment.