Skip to content
Permalink
Browse files

[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!)
  • Loading branch information
nyalldawson committed Dec 20, 2018
1 parent 620baa0 commit a2b5008b30e619bb674d06261e5c07fc654b2799
@@ -525,6 +525,48 @@ will be calculated. This can be expensive to calculate, so if they are not requi
%Docstring
Returns a list of the layers which will be rendered within this map item, considering
any locked layers, linked map theme, and data defined settings.
%End

void addLabelBlockingItem( QgsLayoutItem *item );
%Docstring
Sets the specified layout ``item`` as a "label blocking item" for this map.

Items which are marked as label blocking items prevent any map labels from being placed
in the area of the map item covered by the ``item``.

.. seealso:: :py:func:`removeLabelBlockingItem`

.. seealso:: :py:func:`isLabelBlockingItem`

.. versionadded:: 3.6
%End

void removeLabelBlockingItem( QgsLayoutItem *item );
%Docstring
Removes the specified layout ``item`` from the map's "label blocking items".

Items which are marked as label blocking items prevent any map labels from being placed
in the area of the map item covered by the item.

.. seealso:: :py:func:`addLabelBlockingItem`

.. seealso:: :py:func:`isLabelBlockingItem`

.. versionadded:: 3.6
%End

bool isLabelBlockingItem( QgsLayoutItem *item ) const;
%Docstring
Returns true if the specified ``item`` is a "label blocking item".

Items which are marked as label blocking items prevent any map labels from being placed
in the area of the map item covered by the item.

.. seealso:: :py:func:`addLabelBlockingItem`

.. seealso:: :py:func:`removeLabelBlockingItem`

.. versionadded:: 3.6
%End

protected:
@@ -1742,6 +1742,16 @@ void QgsLayoutMapLabelingWidget::updateGuiElements()
whileBlocking( mLabelBoundaryUnitsCombo )->setUnit( mMapItem->labelMargin().units() );
whileBlocking( mShowPartialLabelsCheckBox )->setChecked( mMapItem->mapFlags() & QgsLayoutItemMap::ShowPartialLabels );

if ( mBlockingItemsListView->model() )
{
QAbstractItemModel *oldModel = mBlockingItemsListView->model();
mBlockingItemsListView->setModel( nullptr );
oldModel->deleteLater();
}

QgsLayoutMapItemBlocksLabelsModel *model = new QgsLayoutMapItemBlocksLabelsModel( mMapItem, mMapItem->layout()->itemsModel(), mBlockingItemsListView );
mBlockingItemsListView->setModel( model );

updateDataDefinedButton( mLabelMarginDDBtn );
}

@@ -1782,3 +1792,108 @@ void QgsLayoutMapLabelingWidget::showPartialsToggled( bool checked )
mMapItem->layout()->undoStack()->endCommand();
mMapItem->invalidateCache();
}

QgsLayoutMapItemBlocksLabelsModel::QgsLayoutMapItemBlocksLabelsModel( QgsLayoutItemMap *map, QgsLayoutModel *layoutModel, QObject *parent )
: QSortFilterProxyModel( parent )
, mLayoutModel( layoutModel )
, mMapItem( map )
{
setSourceModel( layoutModel );
}

int QgsLayoutMapItemBlocksLabelsModel::columnCount( const QModelIndex & ) const
{
return 1;
}

QVariant QgsLayoutMapItemBlocksLabelsModel::data( const QModelIndex &i, int role ) const
{
if ( !i.isValid() )
return QVariant();

if ( i.column() != 0 )
return QVariant();

QModelIndex sourceIndex = mapToSource( index( i.row(), QgsLayoutModel::ItemId, i.parent() ) );

QgsLayoutItem *item = mLayoutModel->itemFromIndex( mapToSource( i ) );
if ( !item )
{
return QVariant();
}

switch ( role )
{
case Qt::CheckStateRole:
switch ( i.column() )
{
case 0:
return mMapItem ? ( mMapItem->isLabelBlockingItem( item ) ? Qt::Checked : Qt::Unchecked ) : Qt::Unchecked;
default:
return QVariant();
}

default:
return mLayoutModel->data( sourceIndex, role );
}
}

bool QgsLayoutMapItemBlocksLabelsModel::setData( const QModelIndex &index, const QVariant &value, int role )
{
Q_UNUSED( role );

if ( !index.isValid() )
return false;

QgsLayoutItem *item = mLayoutModel->itemFromIndex( mapToSource( index ) );
if ( !item || !mMapItem )
{
return false;
}

mMapItem->layout()->undoStack()->beginCommand( mMapItem, tr( "Change Label Blocking Items" ) );

if ( value.toBool() )
{
mMapItem->addLabelBlockingItem( item );
}
else
{
mMapItem->removeLabelBlockingItem( item );
}
emit dataChanged( index, index, QVector<int>() << role );

mMapItem->layout()->undoStack()->endCommand();
mMapItem->invalidateCache();

return true;
}

Qt::ItemFlags QgsLayoutMapItemBlocksLabelsModel::flags( const QModelIndex &index ) const
{
Qt::ItemFlags flags = QAbstractItemModel::flags( index );

if ( ! index.isValid() )
{
return flags ;
}

switch ( index.column() )
{
case 0:
return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
default:
return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
}

bool QgsLayoutMapItemBlocksLabelsModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
{
QgsLayoutItem *item = mLayoutModel->itemFromIndex( mLayoutModel->index( source_row, 0, source_parent ) );
if ( !item || item == mMapItem )
{
return false;
}

return true;
}
@@ -175,6 +175,29 @@ class QgsLayoutMapWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayoutM
};


class QgsLayoutMapItemBlocksLabelsModel : public QSortFilterProxyModel
{
Q_OBJECT

public:

explicit QgsLayoutMapItemBlocksLabelsModel( QgsLayoutItemMap *map, QgsLayoutModel *layoutModel, QObject *parent = nullptr );

int columnCount( const QModelIndex &parent = QModelIndex() ) const override;
QVariant data( const QModelIndex &index, int role ) const override;
bool setData( const QModelIndex &index, const QVariant &value, int role ) override;
Qt::ItemFlags flags( const QModelIndex &index ) const override;

protected:

bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override;

private:
QgsLayoutModel *mLayoutModel = nullptr;
QPointer< QgsLayoutItemMap > mMapItem;

};

/**
* \ingroup app
* Allows configuration of layout map labeling settings.
@@ -610,6 +610,18 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum
mapElem.setAttribute( QStringLiteral( "labelMargin" ), mLabelMargin.encodeMeasurement() );
mapElem.setAttribute( QStringLiteral( "mapFlags" ), static_cast< int>( mMapFlags ) );

QDomElement labelBlockingItemsElem = doc.createElement( QStringLiteral( "labelBlockingItems" ) );
for ( const auto &item : qgis::as_const( mBlockingLabelItems ) )
{
if ( !item )
continue;

QDomElement blockingItemElem = doc.createElement( QStringLiteral( "item" ) );
blockingItemElem.setAttribute( QStringLiteral( "uuid" ), item->uuid() );
labelBlockingItemsElem.appendChild( blockingItemElem );
}
mapElem.appendChild( labelBlockingItemsElem );

return true;
}

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

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

// label blocking items
mBlockingLabelItems.clear();
mBlockingLabelItemUuids.clear();
QDomNodeList labelBlockingNodeList = itemElem.elementsByTagName( QStringLiteral( "labelBlockingItems" ) );
if ( !labelBlockingNodeList.isEmpty() )
{
QDomElement blockingItems = labelBlockingNodeList.at( 0 ).toElement();
QDomNodeList labelBlockingNodeList = blockingItems.childNodes();
for ( int i = 0; i < labelBlockingNodeList.size(); ++i )
{
const QDomElement &itemBlockingElement = labelBlockingNodeList.at( i ).toElement();
const QString itemUuid = itemBlockingElement.attribute( QStringLiteral( "uuid" ) );
mBlockingLabelItemUuids << itemUuid;
}
}

updateBoundingRect();

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

if ( !mBlockingLabelItems.isEmpty() )
{
jobMapSettings.setLabelBlockingRegions( createLabelBlockingRegions( jobMapSettings ) );
}

return jobMapSettings;
}

void QgsLayoutItemMap::finalizeRestoreFromXml()
{
assignFreeId();

mBlockingLabelItems.clear();
for ( const QString &uuid : qgis::as_const( mBlockingLabelItemUuids ) )
{
QgsLayoutItem *item = mLayout->itemByUuid( uuid, true );
if ( item )
{
addLabelBlockingItem( item );
}
}

mOverviewStack->finalizeRestoreFromXml();
mGridStack->finalizeRestoreFromXml();
}
@@ -1254,6 +1297,26 @@ QPolygonF QgsLayoutItemMap::transformedMapPolygon() const
return poly;
}

void QgsLayoutItemMap::addLabelBlockingItem( QgsLayoutItem *item )
{
if ( !mBlockingLabelItems.contains( item ) )
mBlockingLabelItems.append( item );

connect( item, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemMap::invalidateCache, Qt::UniqueConnection );
}

void QgsLayoutItemMap::removeLabelBlockingItem( QgsLayoutItem *item )
{
mBlockingLabelItems.removeAll( item );
if ( item )
disconnect( item, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemMap::invalidateCache );
}

bool QgsLayoutItemMap::isLabelBlockingItem( QgsLayoutItem *item ) const
{
return mBlockingLabelItems.contains( item );
}

QPointF QgsLayoutItemMap::mapToItemCoords( QPointF mapCoords ) const
{
QPolygonF mapPoly = transformedMapPolygon();
@@ -1450,6 +1513,43 @@ void QgsLayoutItemMap::connectUpdateSlot()
connect( project->mapThemeCollection(), &QgsMapThemeCollection::mapThemeChanged, this, &QgsLayoutItemMap::mapThemeChanged );
}

QTransform QgsLayoutItemMap::layoutToMapCoordsTransform() const
{
QPolygonF thisExtent = visibleExtentPolygon();
QTransform mapTransform;
QPolygonF thisRectPoly = QPolygonF( QRectF( 0, 0, rect().width(), rect().height() ) );
//workaround QT Bug #21329
thisRectPoly.pop_back();
thisExtent.pop_back();

QPolygonF thisItemPolyInLayout = mapToScene( thisRectPoly );

//create transform from layout coordinates to map coordinates
QTransform::quadToQuad( thisItemPolyInLayout, thisExtent, mapTransform );
return mapTransform;
}

QList<QgsLabelBlockingRegion> QgsLayoutItemMap::createLabelBlockingRegions( const QgsMapSettings &mapSettings ) const
{
const QTransform mapTransform = layoutToMapCoordsTransform();
QList< QgsLabelBlockingRegion > blockers;
blockers.reserve( mBlockingLabelItems.count() );
for ( const auto &item : qgis::as_const( mBlockingLabelItems ) )
{
if ( !item || !item->isVisible() ) // invisible items don't block labels!
continue;

QPolygonF itemRectInMapCoordinates = mapTransform.map( item->mapToScene( item->rect() ) );
itemRectInMapCoordinates.append( itemRectInMapCoordinates.at( 0 ) ); //close polygon
QgsGeometry blockingRegion = QgsGeometry::fromQPolygonF( itemRectInMapCoordinates );
const double labelMargin = mLayout->convertToLayoutUnits( mEvaluatedLabelMargin );
const double labelMarginInMapUnits = labelMargin / rect().width() * mapSettings.extent().width();
blockingRegion = blockingRegion.buffer( labelMarginInMapUnits, 0, QgsGeometry::CapSquare, QgsGeometry::JoinStyleMiter, 2 );
blockers << QgsLabelBlockingRegion( blockingRegion );
}
return blockers;
}

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

0 comments on commit a2b5008

Please sign in to comment.
You can’t perform that action at this time.