Skip to content

Commit

Permalink
[FEATURE][layouts] Allow layout items to "block" map labels
Browse files Browse the repository at this point in the history
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 a2b5008
Show file tree
Hide file tree
Showing 9 changed files with 556 additions and 11 deletions.
42 changes: 42 additions & 0 deletions python/core/auto_generated/layout/qgslayoutitemmap.sip.in
Expand Up @@ -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:
Expand Down
115 changes: 115 additions & 0 deletions src/app/layout/qgslayoutmapwidget.cpp
Expand Up @@ -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 );
}

Expand Down Expand Up @@ -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;
}
23 changes: 23 additions & 0 deletions src/app/layout/qgslayoutmapwidget.h
Expand Up @@ -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.
Expand Down
100 changes: 100 additions & 0 deletions src/core/layout/qgslayoutitemmap.cpp
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit a2b5008

Please sign in to comment.