Skip to content

Commit a2b5008

Browse files
committed
[FEATURE][layouts] Allow layout items to "block" map labels
This feature allows other layout items (such as scalebars, north arrows, inset maps, etc) to be marked as a blockers for the map labels in a map item. This prevents any map labels from being placed under those items - causing the labeling engine to either try alternative placement for these labels (or discarding them altogether) This allows for more cartographically pleasing maps -- placing labels under other items can make them hard to read, yet without this new setting it's non-trivial to get QGIS to avoid placing the labels in these obscured areas. The blocking items are set through a map item's properties, under the label settings panel. The setting is per-map item, so you can have a scalebar block the labels for one map in your layout and not others (if you so desire!)
1 parent 620baa0 commit a2b5008

File tree

9 files changed

+556
-11
lines changed

9 files changed

+556
-11
lines changed

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

+42
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,48 @@ will be calculated. This can be expensive to calculate, so if they are not requi
525525
%Docstring
526526
Returns a list of the layers which will be rendered within this map item, considering
527527
any locked layers, linked map theme, and data defined settings.
528+
%End
529+
530+
void addLabelBlockingItem( QgsLayoutItem *item );
531+
%Docstring
532+
Sets the specified layout ``item`` as a "label blocking item" for this map.
533+
534+
Items which are marked as label blocking items prevent any map labels from being placed
535+
in the area of the map item covered by the ``item``.
536+
537+
.. seealso:: :py:func:`removeLabelBlockingItem`
538+
539+
.. seealso:: :py:func:`isLabelBlockingItem`
540+
541+
.. versionadded:: 3.6
542+
%End
543+
544+
void removeLabelBlockingItem( QgsLayoutItem *item );
545+
%Docstring
546+
Removes the specified layout ``item`` from the map's "label blocking items".
547+
548+
Items which are marked as label blocking items prevent any map labels from being placed
549+
in the area of the map item covered by the item.
550+
551+
.. seealso:: :py:func:`addLabelBlockingItem`
552+
553+
.. seealso:: :py:func:`isLabelBlockingItem`
554+
555+
.. versionadded:: 3.6
556+
%End
557+
558+
bool isLabelBlockingItem( QgsLayoutItem *item ) const;
559+
%Docstring
560+
Returns true if the specified ``item`` is a "label blocking item".
561+
562+
Items which are marked as label blocking items prevent any map labels from being placed
563+
in the area of the map item covered by the item.
564+
565+
.. seealso:: :py:func:`addLabelBlockingItem`
566+
567+
.. seealso:: :py:func:`removeLabelBlockingItem`
568+
569+
.. versionadded:: 3.6
528570
%End
529571

530572
protected:

src/app/layout/qgslayoutmapwidget.cpp

+115
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,16 @@ void QgsLayoutMapLabelingWidget::updateGuiElements()
17421742
whileBlocking( mLabelBoundaryUnitsCombo )->setUnit( mMapItem->labelMargin().units() );
17431743
whileBlocking( mShowPartialLabelsCheckBox )->setChecked( mMapItem->mapFlags() & QgsLayoutItemMap::ShowPartialLabels );
17441744

1745+
if ( mBlockingItemsListView->model() )
1746+
{
1747+
QAbstractItemModel *oldModel = mBlockingItemsListView->model();
1748+
mBlockingItemsListView->setModel( nullptr );
1749+
oldModel->deleteLater();
1750+
}
1751+
1752+
QgsLayoutMapItemBlocksLabelsModel *model = new QgsLayoutMapItemBlocksLabelsModel( mMapItem, mMapItem->layout()->itemsModel(), mBlockingItemsListView );
1753+
mBlockingItemsListView->setModel( model );
1754+
17451755
updateDataDefinedButton( mLabelMarginDDBtn );
17461756
}
17471757

@@ -1782,3 +1792,108 @@ void QgsLayoutMapLabelingWidget::showPartialsToggled( bool checked )
17821792
mMapItem->layout()->undoStack()->endCommand();
17831793
mMapItem->invalidateCache();
17841794
}
1795+
1796+
QgsLayoutMapItemBlocksLabelsModel::QgsLayoutMapItemBlocksLabelsModel( QgsLayoutItemMap *map, QgsLayoutModel *layoutModel, QObject *parent )
1797+
: QSortFilterProxyModel( parent )
1798+
, mLayoutModel( layoutModel )
1799+
, mMapItem( map )
1800+
{
1801+
setSourceModel( layoutModel );
1802+
}
1803+
1804+
int QgsLayoutMapItemBlocksLabelsModel::columnCount( const QModelIndex & ) const
1805+
{
1806+
return 1;
1807+
}
1808+
1809+
QVariant QgsLayoutMapItemBlocksLabelsModel::data( const QModelIndex &i, int role ) const
1810+
{
1811+
if ( !i.isValid() )
1812+
return QVariant();
1813+
1814+
if ( i.column() != 0 )
1815+
return QVariant();
1816+
1817+
QModelIndex sourceIndex = mapToSource( index( i.row(), QgsLayoutModel::ItemId, i.parent() ) );
1818+
1819+
QgsLayoutItem *item = mLayoutModel->itemFromIndex( mapToSource( i ) );
1820+
if ( !item )
1821+
{
1822+
return QVariant();
1823+
}
1824+
1825+
switch ( role )
1826+
{
1827+
case Qt::CheckStateRole:
1828+
switch ( i.column() )
1829+
{
1830+
case 0:
1831+
return mMapItem ? ( mMapItem->isLabelBlockingItem( item ) ? Qt::Checked : Qt::Unchecked ) : Qt::Unchecked;
1832+
default:
1833+
return QVariant();
1834+
}
1835+
1836+
default:
1837+
return mLayoutModel->data( sourceIndex, role );
1838+
}
1839+
}
1840+
1841+
bool QgsLayoutMapItemBlocksLabelsModel::setData( const QModelIndex &index, const QVariant &value, int role )
1842+
{
1843+
Q_UNUSED( role );
1844+
1845+
if ( !index.isValid() )
1846+
return false;
1847+
1848+
QgsLayoutItem *item = mLayoutModel->itemFromIndex( mapToSource( index ) );
1849+
if ( !item || !mMapItem )
1850+
{
1851+
return false;
1852+
}
1853+
1854+
mMapItem->layout()->undoStack()->beginCommand( mMapItem, tr( "Change Label Blocking Items" ) );
1855+
1856+
if ( value.toBool() )
1857+
{
1858+
mMapItem->addLabelBlockingItem( item );
1859+
}
1860+
else
1861+
{
1862+
mMapItem->removeLabelBlockingItem( item );
1863+
}
1864+
emit dataChanged( index, index, QVector<int>() << role );
1865+
1866+
mMapItem->layout()->undoStack()->endCommand();
1867+
mMapItem->invalidateCache();
1868+
1869+
return true;
1870+
}
1871+
1872+
Qt::ItemFlags QgsLayoutMapItemBlocksLabelsModel::flags( const QModelIndex &index ) const
1873+
{
1874+
Qt::ItemFlags flags = QAbstractItemModel::flags( index );
1875+
1876+
if ( ! index.isValid() )
1877+
{
1878+
return flags ;
1879+
}
1880+
1881+
switch ( index.column() )
1882+
{
1883+
case 0:
1884+
return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
1885+
default:
1886+
return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
1887+
}
1888+
}
1889+
1890+
bool QgsLayoutMapItemBlocksLabelsModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
1891+
{
1892+
QgsLayoutItem *item = mLayoutModel->itemFromIndex( mLayoutModel->index( source_row, 0, source_parent ) );
1893+
if ( !item || item == mMapItem )
1894+
{
1895+
return false;
1896+
}
1897+
1898+
return true;
1899+
}

src/app/layout/qgslayoutmapwidget.h

+23
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,29 @@ class QgsLayoutMapWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayoutM
175175
};
176176

177177

178+
class QgsLayoutMapItemBlocksLabelsModel : public QSortFilterProxyModel
179+
{
180+
Q_OBJECT
181+
182+
public:
183+
184+
explicit QgsLayoutMapItemBlocksLabelsModel( QgsLayoutItemMap *map, QgsLayoutModel *layoutModel, QObject *parent = nullptr );
185+
186+
int columnCount( const QModelIndex &parent = QModelIndex() ) const override;
187+
QVariant data( const QModelIndex &index, int role ) const override;
188+
bool setData( const QModelIndex &index, const QVariant &value, int role ) override;
189+
Qt::ItemFlags flags( const QModelIndex &index ) const override;
190+
191+
protected:
192+
193+
bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override;
194+
195+
private:
196+
QgsLayoutModel *mLayoutModel = nullptr;
197+
QPointer< QgsLayoutItemMap > mMapItem;
198+
199+
};
200+
178201
/**
179202
* \ingroup app
180203
* Allows configuration of layout map labeling settings.

src/core/layout/qgslayoutitemmap.cpp

+100
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,18 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum
610610
mapElem.setAttribute( QStringLiteral( "labelMargin" ), mLabelMargin.encodeMeasurement() );
611611
mapElem.setAttribute( QStringLiteral( "mapFlags" ), static_cast< int>( mMapFlags ) );
612612

613+
QDomElement labelBlockingItemsElem = doc.createElement( QStringLiteral( "labelBlockingItems" ) );
614+
for ( const auto &item : qgis::as_const( mBlockingLabelItems ) )
615+
{
616+
if ( !item )
617+
continue;
618+
619+
QDomElement blockingItemElem = doc.createElement( QStringLiteral( "item" ) );
620+
blockingItemElem.setAttribute( QStringLiteral( "uuid" ), item->uuid() );
621+
labelBlockingItemsElem.appendChild( blockingItemElem );
622+
}
623+
mapElem.appendChild( labelBlockingItemsElem );
624+
613625
return true;
614626
}
615627

@@ -747,6 +759,22 @@ bool QgsLayoutItemMap::readPropertiesFromElement( const QDomElement &itemElem, c
747759

748760
mMapFlags = static_cast< MapItemFlags>( itemElem.attribute( QStringLiteral( "mapFlags" ), nullptr ).toInt() );
749761

762+
// label blocking items
763+
mBlockingLabelItems.clear();
764+
mBlockingLabelItemUuids.clear();
765+
QDomNodeList labelBlockingNodeList = itemElem.elementsByTagName( QStringLiteral( "labelBlockingItems" ) );
766+
if ( !labelBlockingNodeList.isEmpty() )
767+
{
768+
QDomElement blockingItems = labelBlockingNodeList.at( 0 ).toElement();
769+
QDomNodeList labelBlockingNodeList = blockingItems.childNodes();
770+
for ( int i = 0; i < labelBlockingNodeList.size(); ++i )
771+
{
772+
const QDomElement &itemBlockingElement = labelBlockingNodeList.at( i ).toElement();
773+
const QString itemUuid = itemBlockingElement.attribute( QStringLiteral( "uuid" ) );
774+
mBlockingLabelItemUuids << itemUuid;
775+
}
776+
}
777+
750778
updateBoundingRect();
751779

752780
mUpdatesEnabled = true;
@@ -1160,13 +1188,28 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF
11601188
jobMapSettings.setLabelBoundaryGeometry( mapBoundaryGeom );
11611189
}
11621190

1191+
if ( !mBlockingLabelItems.isEmpty() )
1192+
{
1193+
jobMapSettings.setLabelBlockingRegions( createLabelBlockingRegions( jobMapSettings ) );
1194+
}
1195+
11631196
return jobMapSettings;
11641197
}
11651198

11661199
void QgsLayoutItemMap::finalizeRestoreFromXml()
11671200
{
11681201
assignFreeId();
11691202

1203+
mBlockingLabelItems.clear();
1204+
for ( const QString &uuid : qgis::as_const( mBlockingLabelItemUuids ) )
1205+
{
1206+
QgsLayoutItem *item = mLayout->itemByUuid( uuid, true );
1207+
if ( item )
1208+
{
1209+
addLabelBlockingItem( item );
1210+
}
1211+
}
1212+
11701213
mOverviewStack->finalizeRestoreFromXml();
11711214
mGridStack->finalizeRestoreFromXml();
11721215
}
@@ -1254,6 +1297,26 @@ QPolygonF QgsLayoutItemMap::transformedMapPolygon() const
12541297
return poly;
12551298
}
12561299

1300+
void QgsLayoutItemMap::addLabelBlockingItem( QgsLayoutItem *item )
1301+
{
1302+
if ( !mBlockingLabelItems.contains( item ) )
1303+
mBlockingLabelItems.append( item );
1304+
1305+
connect( item, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemMap::invalidateCache, Qt::UniqueConnection );
1306+
}
1307+
1308+
void QgsLayoutItemMap::removeLabelBlockingItem( QgsLayoutItem *item )
1309+
{
1310+
mBlockingLabelItems.removeAll( item );
1311+
if ( item )
1312+
disconnect( item, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemMap::invalidateCache );
1313+
}
1314+
1315+
bool QgsLayoutItemMap::isLabelBlockingItem( QgsLayoutItem *item ) const
1316+
{
1317+
return mBlockingLabelItems.contains( item );
1318+
}
1319+
12571320
QPointF QgsLayoutItemMap::mapToItemCoords( QPointF mapCoords ) const
12581321
{
12591322
QPolygonF mapPoly = transformedMapPolygon();
@@ -1450,6 +1513,43 @@ void QgsLayoutItemMap::connectUpdateSlot()
14501513
connect( project->mapThemeCollection(), &QgsMapThemeCollection::mapThemeChanged, this, &QgsLayoutItemMap::mapThemeChanged );
14511514
}
14521515

1516+
QTransform QgsLayoutItemMap::layoutToMapCoordsTransform() const
1517+
{
1518+
QPolygonF thisExtent = visibleExtentPolygon();
1519+
QTransform mapTransform;
1520+
QPolygonF thisRectPoly = QPolygonF( QRectF( 0, 0, rect().width(), rect().height() ) );
1521+
//workaround QT Bug #21329
1522+
thisRectPoly.pop_back();
1523+
thisExtent.pop_back();
1524+
1525+
QPolygonF thisItemPolyInLayout = mapToScene( thisRectPoly );
1526+
1527+
//create transform from layout coordinates to map coordinates
1528+
QTransform::quadToQuad( thisItemPolyInLayout, thisExtent, mapTransform );
1529+
return mapTransform;
1530+
}
1531+
1532+
QList<QgsLabelBlockingRegion> QgsLayoutItemMap::createLabelBlockingRegions( const QgsMapSettings &mapSettings ) const
1533+
{
1534+
const QTransform mapTransform = layoutToMapCoordsTransform();
1535+
QList< QgsLabelBlockingRegion > blockers;
1536+
blockers.reserve( mBlockingLabelItems.count() );
1537+
for ( const auto &item : qgis::as_const( mBlockingLabelItems ) )
1538+
{
1539+
if ( !item || !item->isVisible() ) // invisible items don't block labels!
1540+
continue;
1541+
1542+
QPolygonF itemRectInMapCoordinates = mapTransform.map( item->mapToScene( item->rect() ) );
1543+
itemRectInMapCoordinates.append( itemRectInMapCoordinates.at( 0 ) ); //close polygon
1544+
QgsGeometry blockingRegion = QgsGeometry::fromQPolygonF( itemRectInMapCoordinates );
1545+
const double labelMargin = mLayout->convertToLayoutUnits( mEvaluatedLabelMargin );
1546+
const double labelMarginInMapUnits = labelMargin / rect().width() * mapSettings.extent().width();
1547+
blockingRegion = blockingRegion.buffer( labelMarginInMapUnits, 0, QgsGeometry::CapSquare, QgsGeometry::JoinStyleMiter, 2 );
1548+
blockers << QgsLabelBlockingRegion( blockingRegion );
1549+
}
1550+
return blockers;
1551+
}
1552+
14531553
QgsLayoutMeasurement QgsLayoutItemMap::labelMargin() const
14541554
{
14551555
return mLabelMargin;

0 commit comments

Comments
 (0)