Skip to content
Permalink
Browse files

[layouts] Add API to set item based clipping region on maps

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 264bd5159406d8e1d467d66d5bc701d9a5080f97
@@ -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;
@@ -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();
@@ -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;
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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.
You can’t perform that action at this time.