[feature][layouts] Add automatic clipping settings for atlas maps

This feature allows users to enable map clipping for layout map items
so that the maps are clipped to the boundary of the current atlas feature.

(It's available for polygon atlas coverage layers only, for obvious reasons!)

Options exist for:
- Enabling or disabling the clipping on a per-map basis
- Specifying the clipping type:
   - "Clip During Render Only": applies a painter based clip, so that
     portions of vector features which sit outside the atlas feature become
     invisible
   - "Clip Feature Before Render": applies the clip before rendering features,
     so borders of features which fall partially outside the atlas feature
     will still be visible on the boundary of the atlas feature
   - "Render Intersecting Features Unchanged": just renders all features
     which intersect the current atlas feature, but without clipping their
     geometry
- Controlling whether labels should be forced placed inside the atlas feature,
or whether they may be placed outside the feature
- Restricting the clip to a subset of the layers in the project, so that
only some are clipped

Sponsored by City of Canning
This commit is contained in:
Nyall Dawson 2020-07-03 08:49:29 +10:00
parent 329a0fc110
commit c30c769ef5
15 changed files with 1005 additions and 37 deletions

View File

@ -9,6 +9,110 @@
class QgsLayoutItemMapAtlasClippingSettings : QObject
{
%Docstring
Contains settings relating to clipping a layout map by the current atlas feature.
.. versionadded:: 3.16
%End
%TypeHeaderCode
#include "qgslayoutitemmap.h"
%End
public:
QgsLayoutItemMapAtlasClippingSettings( QgsLayoutItemMap *map /TransferThis/ = 0 );
%Docstring
Constructor for QgsLayoutItemMapAtlasClippingSettings, with the specified ``map`` parent.
%End
bool enabled() const;
%Docstring
Returns ``True`` if the map content should be clipped to the current atlas feature.
.. seealso:: :py:func:`setEnabled`
%End
void setEnabled( bool enabled );
%Docstring
Sets whether the map content should be clipped to the current atlas feature.
.. seealso:: :py:func:`enabled`
%End
QgsMapClippingRegion::FeatureClippingType featureClippingType() const;
%Docstring
Returns the feature clipping type to apply when clipping to the current atlas feature.
.. seealso:: :py:func:`setFeatureClippingType`
%End
void setFeatureClippingType( QgsMapClippingRegion::FeatureClippingType type );
%Docstring
Sets the feature clipping ``type`` to apply when clipping to the current atlas feature.
.. seealso:: :py:func:`featureClippingType`
%End
bool forceLabelsInsideFeature() const;
%Docstring
Returns ``True`` if labels should only be placed inside the atlas feature geometry.
.. seealso:: :py:func:`setForceLabelsInsideFeature`
%End
void setForceLabelsInsideFeature( bool forceInside );
%Docstring
Sets whether labels should only be placed inside the atlas feature geometry.
.. seealso:: :py:func:`forceLabelsInsideFeature`
%End
QList< QgsMapLayer * > layersToClip() const;
%Docstring
Returns the list of map layers to clip to the atlas feature.
If the returned list is empty then all layers will be clipped.
.. seealso:: :py:func:`setLayersToClip`
%End
void setLayersToClip( const QList< QgsMapLayer * > &layers );
%Docstring
Sets the list of map ``layers`` to clip to the atlas feature.
If the ``layers`` list is empty then all layers will be clipped.
.. seealso:: :py:func:`layersToClip`
%End
bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Stores settings in a DOM element, where ``element`` is the DOM element
corresponding to a 'LayoutMap' tag.
.. seealso:: :py:func:`readXml`
%End
bool readXml( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context );
%Docstring
Sets the setting's state from a DOM document, where ``element`` is the DOM
node corresponding to a 'LayoutMap' tag.
.. seealso:: :py:func:`writeXml`
%End
signals:
void changed();
%Docstring
Emitted when the atlas clipping settings are changed.
%End
};
class QgsLayoutItemMap : QgsLayoutItem, QgsTemporalRangeObject
{
%Docstring
@ -621,6 +725,13 @@ Removes a previously added rendered feature ``handler``.
QTransform layoutToMapCoordsTransform() const;
%Docstring
Creates a transform from layout coordinates to map coordinates.
%End
QgsLayoutItemMapAtlasClippingSettings *atlasClippingSettings();
%Docstring
Returns the map's atlas clipping settings.
.. versionadded:: 3.16
%End
protected:

View File

@ -119,6 +119,12 @@ Returns ``True`` if the model includes layer's CRS in the display role.
%Docstring
layersChecked returns the list of layers which are checked (or unchecked)
%End
void setLayersChecked( const QList< QgsMapLayer * > &layers );
%Docstring
Sets which layers are checked in the model.
%End
bool itemsCheckable() const;
%Docstring
returns if the items can be checked or not

View File

@ -39,6 +39,7 @@
QgsLayoutItemMap::QgsLayoutItemMap( QgsLayout *layout )
: QgsLayoutItem( layout )
, mAtlasClippingSettings( new QgsLayoutItemMapAtlasClippingSettings( this ) )
{
mBackgroundUpdateTimer = new QTimer( this );
mBackgroundUpdateTimer->setSingleShot( true );
@ -56,6 +57,11 @@ QgsLayoutItemMap::QgsLayoutItemMap( QgsLayout *layout )
mGridStack = qgis::make_unique< QgsLayoutItemMapGridStack >( this );
mOverviewStack = qgis::make_unique< QgsLayoutItemMapOverviewStack >( this );
connect( mAtlasClippingSettings, &QgsLayoutItemMapAtlasClippingSettings::changed, this, [ = ]
{
refresh();
} );
if ( layout )
connectUpdateSlot();
}
@ -657,6 +663,8 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum
mapElem.setAttribute( QStringLiteral( "temporalRangeEnd" ), temporalRange().end().toString( Qt::ISODate ) );
}
mAtlasClippingSettings->writeXml( mapElem, doc, context );
return true;
}
@ -811,6 +819,8 @@ bool QgsLayoutItemMap::readPropertiesFromElement( const QDomElement &itemElem, c
}
}
mAtlasClippingSettings->readXml( itemElem, doc, context );
updateBoundingRect();
//temporal settings
@ -1477,6 +1487,24 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF
if ( isTemporal() )
jobMapSettings.setTemporalRange( temporalRange() );
if ( mAtlasClippingSettings->enabled() && mLayout->reportContext().feature().isValid() )
{
QgsGeometry clipGeom( mLayout->reportContext().currentGeometry( jobMapSettings.destinationCrs() ) );
QgsMapClippingRegion region( clipGeom );
region.setFeatureClip( mAtlasClippingSettings->featureClippingType() );
region.setRestrictedLayers( mAtlasClippingSettings->layersToClip() );
jobMapSettings.addClippingRegion( region );
if ( mAtlasClippingSettings->forceLabelsInsideFeature() )
{
if ( !jobMapSettings.labelBoundaryGeometry().isEmpty() )
{
clipGeom = clipGeom.intersection( jobMapSettings.labelBoundaryGeometry() );
}
jobMapSettings.setLabelBoundaryGeometry( clipGeom );
}
}
return jobMapSettings;
}
@ -2606,3 +2634,140 @@ void QgsLayoutItemMap::createStagedRenderJob( const QgsRectangle &extent, const
}
//
// QgsLayoutItemMapAtlasClippingSettings
//
QgsLayoutItemMapAtlasClippingSettings::QgsLayoutItemMapAtlasClippingSettings( QgsLayoutItemMap *map )
: QObject( map )
, mMap( map )
{
if ( mMap->layout() && mMap->layout()->project() )
{
connect( mMap->layout()->project(), static_cast < void ( QgsProject::* )( const QList<QgsMapLayer *>& layers ) > ( &QgsProject::layersWillBeRemoved ),
this, &QgsLayoutItemMapAtlasClippingSettings::layersAboutToBeRemoved );
}
}
bool QgsLayoutItemMapAtlasClippingSettings::enabled() const
{
return mClipToAtlasFeature;
}
void QgsLayoutItemMapAtlasClippingSettings::setEnabled( bool enabled )
{
if ( enabled == mClipToAtlasFeature )
return;
mClipToAtlasFeature = enabled;
emit changed();
}
QgsMapClippingRegion::FeatureClippingType QgsLayoutItemMapAtlasClippingSettings::featureClippingType() const
{
return mFeatureClippingType;
}
void QgsLayoutItemMapAtlasClippingSettings::setFeatureClippingType( QgsMapClippingRegion::FeatureClippingType type )
{
if ( mFeatureClippingType == type )
return;
mFeatureClippingType = type;
emit changed();
}
bool QgsLayoutItemMapAtlasClippingSettings::forceLabelsInsideFeature() const
{
return mForceLabelsInsideFeature;
}
void QgsLayoutItemMapAtlasClippingSettings::setForceLabelsInsideFeature( bool forceInside )
{
if ( forceInside == mForceLabelsInsideFeature )
return;
mForceLabelsInsideFeature = forceInside;
emit changed();
}
QList<QgsMapLayer *> QgsLayoutItemMapAtlasClippingSettings::layersToClip() const
{
return _qgis_listRefToRaw( mLayersToClip );
}
void QgsLayoutItemMapAtlasClippingSettings::setLayersToClip( const QList< QgsMapLayer * > &layersToClip )
{
mLayersToClip = _qgis_listRawToRef( layersToClip );
emit changed();
}
bool QgsLayoutItemMapAtlasClippingSettings::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext & ) const
{
QDomElement settingsElem = document.createElement( QStringLiteral( "atlasClippingSettings" ) );
settingsElem.setAttribute( QStringLiteral( "enabled" ), mClipToAtlasFeature ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
settingsElem.setAttribute( QStringLiteral( "forceLabelsInside" ), mForceLabelsInsideFeature ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
settingsElem.setAttribute( QStringLiteral( "clippingType" ), QString::number( static_cast<int>( mFeatureClippingType ) ) );
//layer set
QDomElement layerSetElem = document.createElement( QStringLiteral( "layersToClip" ) );
for ( const QgsMapLayerRef &layerRef : mLayersToClip )
{
if ( !layerRef )
continue;
QDomElement layerElem = document.createElement( QStringLiteral( "Layer" ) );
QDomText layerIdText = document.createTextNode( layerRef.layerId );
layerElem.appendChild( layerIdText );
layerElem.setAttribute( QStringLiteral( "name" ), layerRef.name );
layerElem.setAttribute( QStringLiteral( "source" ), layerRef.source );
layerElem.setAttribute( QStringLiteral( "provider" ), layerRef.provider );
layerSetElem.appendChild( layerElem );
}
settingsElem.appendChild( layerSetElem );
element.appendChild( settingsElem );
return true;
}
bool QgsLayoutItemMapAtlasClippingSettings::readXml( const QDomElement &element, const QDomDocument &, const QgsReadWriteContext & )
{
const QDomElement settingsElem = element.firstChildElement( QStringLiteral( "atlasClippingSettings" ) );
mClipToAtlasFeature = settingsElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt();
mForceLabelsInsideFeature = settingsElem.attribute( QStringLiteral( "forceLabelsInside" ), QStringLiteral( "0" ) ).toInt();
mFeatureClippingType = static_cast< QgsMapClippingRegion::FeatureClippingType >( settingsElem.attribute( QStringLiteral( "clippingType" ), QStringLiteral( "0" ) ).toInt() );
mLayersToClip.clear();
QDomNodeList layerSetNodeList = settingsElem.elementsByTagName( QStringLiteral( "layersToClip" ) );
if ( !layerSetNodeList.isEmpty() )
{
QDomElement layerSetElem = layerSetNodeList.at( 0 ).toElement();
QDomNodeList layerIdNodeList = layerSetElem.elementsByTagName( QStringLiteral( "Layer" ) );
mLayersToClip.reserve( layerIdNodeList.size() );
for ( int i = 0; i < layerIdNodeList.size(); ++i )
{
QDomElement layerElem = layerIdNodeList.at( i ).toElement();
QString layerId = layerElem.text();
QString layerName = layerElem.attribute( QStringLiteral( "name" ) );
QString layerSource = layerElem.attribute( QStringLiteral( "source" ) );
QString layerProvider = layerElem.attribute( QStringLiteral( "provider" ) );
QgsMapLayerRef ref( layerId, layerName, layerSource, layerProvider );
if ( mMap->layout() && mMap->layout()->project() )
ref.resolveWeakly( mMap->layout()->project() );
mLayersToClip << ref;
}
}
return true;
}
void QgsLayoutItemMapAtlasClippingSettings::layersAboutToBeRemoved( const QList<QgsMapLayer *> &layers )
{
if ( !mLayersToClip.isEmpty() )
{
_qgis_removeLayers( mLayersToClip, layers );
}
}

View File

@ -30,6 +30,117 @@
class QgsAnnotation;
class QgsRenderedFeatureHandlerInterface;
/**
* \ingroup core
* \class QgsLayoutItemMapAtlasClippingSettings
* \brief Contains settings relating to clipping a layout map by the current atlas feature.
* \since QGIS 3.16
*/
class CORE_EXPORT QgsLayoutItemMapAtlasClippingSettings : public QObject
{
Q_OBJECT
public:
/**
* Constructor for QgsLayoutItemMapAtlasClippingSettings, with the specified \a map parent.
*/
QgsLayoutItemMapAtlasClippingSettings( QgsLayoutItemMap *map SIP_TRANSFERTHIS = nullptr );
/**
* Returns TRUE if the map content should be clipped to the current atlas feature.
*
* \see setEnabled()
*/
bool enabled() const;
/**
* Sets whether the map content should be clipped to the current atlas feature.
*
* \see enabled()
*/
void setEnabled( bool enabled );
/**
* Returns the feature clipping type to apply when clipping to the current atlas feature.
*
* \see setFeatureClippingType()
*/
QgsMapClippingRegion::FeatureClippingType featureClippingType() const;
/**
* Sets the feature clipping \a type to apply when clipping to the current atlas feature.
*
* \see featureClippingType()
*/
void setFeatureClippingType( QgsMapClippingRegion::FeatureClippingType type );
/**
* Returns TRUE if labels should only be placed inside the atlas feature geometry.
*
* \see setForceLabelsInsideFeature()
*/
bool forceLabelsInsideFeature() const;
/**
* Sets whether labels should only be placed inside the atlas feature geometry.
*
* \see forceLabelsInsideFeature()
*/
void setForceLabelsInsideFeature( bool forceInside );
/**
* Returns the list of map layers to clip to the atlas feature.
*
* If the returned list is empty then all layers will be clipped.
*
* \see setLayersToClip()
*/
QList< QgsMapLayer * > layersToClip() const;
/**
* Sets the list of map \a layers to clip to the atlas feature.
*
* If the \a layers list is empty then all layers will be clipped.
*
* \see layersToClip()
*/
void setLayersToClip( const QList< QgsMapLayer * > &layers );
/**
* Stores settings in a DOM element, where \a element is the DOM element
* corresponding to a 'LayoutMap' tag.
* \see readXml()
*/
bool writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
/**
* Sets the setting's state from a DOM document, where \a element is the DOM
* node corresponding to a 'LayoutMap' tag.
* \see writeXml()
*/
bool readXml( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context );
signals:
/**
* Emitted when the atlas clipping settings are changed.
*/
void changed();
private slots:
void layersAboutToBeRemoved( const QList<QgsMapLayer *> &layers );
private:
QgsLayoutItemMap *mMap = nullptr;
bool mClipToAtlasFeature = false;
QList< QgsMapLayerRef > mLayersToClip;
QgsMapClippingRegion::FeatureClippingType mFeatureClippingType = QgsMapClippingRegion::FeatureClippingType::ClipPainterOnly;
bool mForceLabelsInsideFeature = false;
};
/**
* \ingroup core
* \class QgsLayoutItemMap
@ -559,6 +670,13 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem, public QgsTemporalRan
*/
QTransform layoutToMapCoordsTransform() const;
/**
* Returns the map's atlas clipping settings.
*
* \since QGIS 3.16
*/
QgsLayoutItemMapAtlasClippingSettings *atlasClippingSettings() { return mAtlasClippingSettings; }
protected:
void draw( QgsLayoutItemRenderContext &context ) override;
@ -843,6 +961,8 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem, public QgsTemporalRan
QStringList mExportThemes;
QStringList::iterator mExportThemeIt;
QgsLayoutItemMapAtlasClippingSettings *mAtlasClippingSettings = nullptr;
/**
* Refresh the map's extents, considering data defined extent, scale and rotation
* \param context expression context for evaluating data defined map parameters

View File

@ -105,6 +105,20 @@ QList<QgsMapLayer *> QgsMapLayerModel::layersChecked( Qt::CheckState checkState
return layers;
}
void QgsMapLayerModel::setLayersChecked( const QList<QgsMapLayer *> &layers )
{
QMap<QString, Qt::CheckState>::iterator i = mLayersChecked.begin();
for ( ; i != mLayersChecked.end(); ++i )
{
*i = Qt::Unchecked;
}
for ( const QgsMapLayer *layer : layers )
{
mLayersChecked[ layer->id() ] = Qt::Checked;
}
emit dataChanged( index( 0, 0 ), index( rowCount() - 1, 0 ), QVector<int>() << Qt::CheckStateRole );
}
QModelIndex QgsMapLayerModel::indexFromLayer( QgsMapLayer *layer ) const
{
int r = mLayers.indexOf( layer );
@ -569,7 +583,7 @@ bool QgsMapLayerModel::setData( const QModelIndex &index, const QVariant &value,
{
QgsMapLayer *layer = static_cast<QgsMapLayer *>( index.internalPointer() );
mLayersChecked[layer->id()] = ( Qt::CheckState )value.toInt();
emit dataChanged( index, index );
emit dataChanged( index, index, QVector< int >() << Qt::CheckStateRole );
return true;
}
break;

View File

@ -128,6 +128,12 @@ class CORE_EXPORT QgsMapLayerModel : public QAbstractItemModel
* \brief layersChecked returns the list of layers which are checked (or unchecked)
*/
QList<QgsMapLayer *> layersChecked( Qt::CheckState checkState = Qt::Checked );
/**
* Sets which layers are checked in the model.
*/
void setLayersChecked( const QList< QgsMapLayer * > &layers );
//! returns if the items can be checked or not
bool itemsCheckable() const { return mItemCheckable; }

View File

@ -34,6 +34,7 @@
#include "qgsbookmarkmodel.h"
#include "qgsreferencedgeometry.h"
#include "qgsprojectviewsettings.h"
#include "qgsmaplayermodel.h"
#include <QMenu>
#include <QMessageBox>
@ -85,6 +86,8 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item, QgsMapCanvas *ma
connect( mOverviewListWidget, &QListWidget::currentItemChanged, this, &QgsLayoutMapWidget::mOverviewListWidget_currentItemChanged );
connect( mOverviewListWidget, &QListWidget::itemChanged, this, &QgsLayoutMapWidget::mOverviewListWidget_itemChanged );
connect( mActionLabelSettings, &QAction::triggered, this, &QgsLayoutMapWidget::showLabelSettings );
connect( mActionClipSettings, &QAction::triggered, this, &QgsLayoutMapWidget::showClipSettings );
connect( mTemporalCheckBox, &QgsCollapsibleGroupBoxBasic::toggled, this, &QgsLayoutMapWidget::mTemporalCheckBox_toggled );
connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsLayoutMapWidget::updateTemporalExtent );
connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsLayoutMapWidget::updateTemporalExtent );
@ -207,8 +210,14 @@ void QgsLayoutMapWidget::setMasterLayout( QgsMasterLayoutInterface *masterLayout
void QgsLayoutMapWidget::setReportTypeString( const QString &string )
{
mReportTypeString = string;
mAtlasCheckBox->setTitle( tr( "Controlled by %1" ).arg( string == tr( "atlas" ) ? tr( "Atlas" ) : tr( "Report" ) ) );
mAtlasPredefinedScaleRadio->setToolTip( tr( "Use one of the predefined scales of the project where the %1 feature best fits." ).arg( string ) );
if ( mClipWidget )
mClipWidget->setReportTypeString( string );
if ( mLabelWidget )
mLabelWidget->setReportTypeString( string );
}
void QgsLayoutMapWidget::setDesignerInterface( QgsLayoutDesignerInterface *iface )
@ -233,6 +242,8 @@ bool QgsLayoutMapWidget::setNewItem( QgsLayoutItem *item )
mItemPropertiesWidget->setItem( mMapItem );
if ( mLabelWidget )
mLabelWidget->setItem( mMapItem );
if ( mClipWidget )
mClipWidget->setItem( mMapItem );
if ( mMapItem )
{
@ -413,9 +424,21 @@ void QgsLayoutMapWidget::overviewSymbolChanged()
void QgsLayoutMapWidget::showLabelSettings()
{
mLabelWidget = new QgsLayoutMapLabelingWidget( mMapItem );
if ( !mReportTypeString.isEmpty() )
mLabelWidget->setReportTypeString( mReportTypeString );
openPanel( mLabelWidget );
}
void QgsLayoutMapWidget::showClipSettings()
{
mClipWidget = new QgsLayoutMapClippingWidget( mMapItem );
if ( !mReportTypeString.isEmpty() )
mClipWidget->setReportTypeString( mReportTypeString );
openPanel( mClipWidget );
}
void QgsLayoutMapWidget::switchToMoveContentTool()
{
if ( mInterface )
@ -1925,3 +1948,180 @@ bool QgsLayoutMapItemBlocksLabelsModel::filterAcceptsRow( int source_row, const
return true;
}
//
// QgsLayoutMapClippingWidget
//
QgsLayoutMapClippingWidget::QgsLayoutMapClippingWidget( QgsLayoutItemMap *map )
: QgsLayoutItemBaseWidget( nullptr, map )
, mMapItem( map )
{
setupUi( this );
setPanelTitle( tr( "Clipping Settings" ) );
mLayerModel = new QgsMapLayerModel( this );
mLayerModel->setItemsCheckable( true );
mLayersTreeView->setModel( mLayerModel );
mAtlasClippingTypeComboBox->addItem( tr( "Clip During Render Only" ), static_cast< int >( QgsMapClippingRegion::FeatureClippingType::ClipPainterOnly ) );
mAtlasClippingTypeComboBox->addItem( tr( "Clip Feature Before Render" ), static_cast< int >( QgsMapClippingRegion::FeatureClippingType::ClipToIntersection ) );
mAtlasClippingTypeComboBox->addItem( tr( "Render Intersecting Features Unchanged" ), static_cast< int >( QgsMapClippingRegion::FeatureClippingType::NoClipping ) );
connect( mRadioClipSelectedLayers, &QRadioButton::toggled, mLayersTreeView, &QWidget::setEnabled );
mLayersTreeView->setEnabled( false );
mRadioClipAllLayers->setChecked( true );
connect( mClipToAtlasCheckBox, &QGroupBox::toggled, this, [ = ]( bool active )
{
if ( !mBlockUpdates )
{
mMapItem->beginCommand( tr( "Toggle Atlas Clipping" ) );
mMapItem->atlasClippingSettings()->setEnabled( active );
mMapItem->endCommand();
}
} );
connect( mForceLabelsInsideCheckBox, &QCheckBox::toggled, this, [ = ]( bool active )
{
if ( !mBlockUpdates )
{
mMapItem->beginCommand( tr( "Change Atlas Clipping Label Behavior" ) );
mMapItem->atlasClippingSettings()->setForceLabelsInsideFeature( active );
mMapItem->endCommand();
}
} );
connect( mAtlasClippingTypeComboBox, qgis::overload<int>::of( &QComboBox::currentIndexChanged ), this, [ = ]
{
if ( !mBlockUpdates )
{
mMapItem->beginCommand( tr( "Change Atlas Clipping Behavior" ) );
mMapItem->atlasClippingSettings()->setFeatureClippingType( static_cast< QgsMapClippingRegion::FeatureClippingType >( mAtlasClippingTypeComboBox->currentData().toInt() ) );
mMapItem->endCommand();
}
} );
connect( mRadioClipSelectedLayers, &QCheckBox::toggled, this, [ = ]( bool active )
{
if ( active && !mBlockUpdates )
{
mBlockUpdates = true;
mMapItem->beginCommand( tr( "Change Atlas Clipping Layers" ) );
mMapItem->atlasClippingSettings()->setLayersToClip( mLayerModel->layersChecked( ) );
mMapItem->endCommand();
mBlockUpdates = false;
}
} );
connect( mRadioClipAllLayers, &QCheckBox::toggled, this, [ = ]( bool active )
{
if ( active && !mBlockUpdates )
{
mBlockUpdates = true;
mMapItem->beginCommand( tr( "Change Atlas Clipping Layers" ) );
mMapItem->atlasClippingSettings()->setLayersToClip( QList< QgsMapLayer * >() );
mMapItem->endCommand();
mBlockUpdates = false;
}
} );
connect( mLayerModel, &QgsMapLayerModel::dataChanged, this, [ = ]( const QModelIndex &, const QModelIndex &, const QVector<int> &roles = QVector<int>() )
{
if ( !roles.contains( Qt::CheckStateRole ) )
return;
if ( !mBlockUpdates )
{
mBlockUpdates = true;
mMapItem->beginCommand( tr( "Change Atlas Clipping Layers" ) );
mMapItem->atlasClippingSettings()->setLayersToClip( mLayerModel->layersChecked() );
mMapItem->endCommand();
mBlockUpdates = false;
}
} );
setNewItem( map );
connect( &map->layout()->reportContext(), &QgsLayoutReportContext::layerChanged,
this, &QgsLayoutMapClippingWidget::atlasLayerChanged );
if ( QgsLayoutAtlas *atlas = layoutAtlas() )
{
connect( atlas, &QgsLayoutAtlas::toggled, this, &QgsLayoutMapClippingWidget::atlasToggled );
atlasToggled( atlas->enabled() );
}
}
void QgsLayoutMapClippingWidget::setReportTypeString( const QString &string )
{
mClipToAtlasCheckBox->setTitle( tr( "Clip to %1 feature" ).arg( string ) );
mClipToAtlasLabel->setText( tr( "<b>When enabled, map layers will be automatically clipped to the boundary of the current %1 feature.</b>" ).arg( string ) );
mForceLabelsInsideCheckBox->setText( tr( "Force labels inside %1 feature" ).arg( string ) );
}
bool QgsLayoutMapClippingWidget::setNewItem( QgsLayoutItem *item )
{
if ( item->type() != QgsLayoutItemRegistry::LayoutMap )
return false;
if ( mMapItem )
{
disconnect( mMapItem, &QgsLayoutObject::changed, this, &QgsLayoutMapClippingWidget::updateGuiElements );
}
mMapItem = qobject_cast< QgsLayoutItemMap * >( item );
if ( mMapItem )
{
connect( mMapItem, &QgsLayoutObject::changed, this, &QgsLayoutMapClippingWidget::updateGuiElements );
}
updateGuiElements();
return true;
}
void QgsLayoutMapClippingWidget::updateGuiElements()
{
if ( mBlockUpdates )
return;
mBlockUpdates = true;
mClipToAtlasCheckBox->setChecked( mMapItem->atlasClippingSettings()->enabled() );
mAtlasClippingTypeComboBox->setCurrentIndex( mAtlasClippingTypeComboBox->findData( static_cast< int >( mMapItem->atlasClippingSettings()->featureClippingType() ) ) );
mForceLabelsInsideCheckBox->setChecked( mMapItem->atlasClippingSettings()->forceLabelsInsideFeature() );
mRadioClipAllLayers->setChecked( mMapItem->atlasClippingSettings()->layersToClip().isEmpty() );
mRadioClipSelectedLayers->setChecked( !mMapItem->atlasClippingSettings()->layersToClip().isEmpty() );
mLayerModel->setLayersChecked( mMapItem->atlasClippingSettings()->layersToClip() );
mBlockUpdates = false;
}
void QgsLayoutMapClippingWidget::atlasLayerChanged( QgsVectorLayer *layer )
{
if ( !layer || layer->geometryType() != QgsWkbTypes::PolygonGeometry )
{
//non-polygon layer, disable atlas control
mClipToAtlasCheckBox->setChecked( false );
mClipToAtlasCheckBox->setEnabled( false );
return;
}
else
{
mClipToAtlasCheckBox->setEnabled( true );
}
}
void QgsLayoutMapClippingWidget::atlasToggled( bool atlasEnabled )
{
if ( atlasEnabled &&
mMapItem && mMapItem->layout() && mMapItem->layout()->reportContext().layer()
&& mMapItem->layout()->reportContext().layer()->geometryType() == QgsWkbTypes::PolygonGeometry )
{
mClipToAtlasCheckBox->setEnabled( true );
}
else
{
mClipToAtlasCheckBox->setEnabled( false );
mClipToAtlasCheckBox->setChecked( false );
}
}

View File

@ -24,6 +24,7 @@
#include "qgis_gui.h"
#include "ui_qgslayoutmapwidgetbase.h"
#include "ui_qgslayoutmaplabelingwidgetbase.h"
#include "ui_qgslayoutmapclippingwidgetbase.h"
#include "qgslayoutitemwidget.h"
#include "qgslayoutitemmapgrid.h"
@ -31,6 +32,7 @@ class QgsMapLayer;
class QgsLayoutItemMap;
class QgsLayoutItemMapOverview;
class QgsLayoutMapLabelingWidget;
class QgsLayoutMapClippingWidget;
class QgsBookmarkManagerProxyModel;
/**
@ -136,6 +138,7 @@ class GUI_EXPORT QgsLayoutMapWidget: public QgsLayoutItemBaseWidget, private Ui:
void mapCrsChanged( const QgsCoordinateReferenceSystem &crs );
void overviewSymbolChanged();
void showLabelSettings();
void showClipSettings();
void switchToMoveContentTool();
void aboutToShowBookmarkMenu();
@ -145,8 +148,10 @@ class GUI_EXPORT QgsLayoutMapWidget: public QgsLayoutItemBaseWidget, private Ui:
QgsLayoutItemPropertiesWidget *mItemPropertiesWidget = nullptr;
QgsLayoutDesignerInterface *mInterface = nullptr;
QPointer< QgsLayoutMapLabelingWidget > mLabelWidget;
QPointer< QgsLayoutMapClippingWidget > mClipWidget;
QMenu *mBookmarkMenu = nullptr;
QgsBookmarkManagerProxyModel *mBookmarkModel = nullptr;
QString mReportTypeString;
//! Sets extent of composer map from line edits
void updateComposerExtentFromGui();
@ -242,4 +247,36 @@ class GUI_EXPORT QgsLayoutMapLabelingWidget: public QgsLayoutItemBaseWidget, pri
QPointer< QgsLayoutItemMap > mMapItem;
};
/**
* \ingroup gui
* Allows configuration of layout map clipping settings.
*
* \note This class is not a part of public API
* \since QGIS 3.16
*/
class GUI_EXPORT QgsLayoutMapClippingWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayoutMapClippingWidgetBase
{
Q_OBJECT
public:
//! constructor
explicit QgsLayoutMapClippingWidget( QgsLayoutItemMap *map );
void setReportTypeString( const QString &string ) override;
protected:
bool setNewItem( QgsLayoutItem *item ) override;
private slots:
void updateGuiElements();
void atlasLayerChanged( QgsVectorLayer *layer );
void atlasToggled( bool atlasEnabled );
private:
QPointer< QgsLayoutItemMap > mMapItem;
QgsMapLayerModel *mLayerModel = nullptr;
bool mBlockUpdates = false;
};
#endif

View File

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QgsLayoutMapClippingWidgetBase</class>
<widget class="QWidget" name="QgsLayoutMapClippingWidgetBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>318</width>
<height>408</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Map Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QgsCollapsibleGroupBox" name="mClipToAtlasCheckBox">
<property name="title">
<string>Clip to atlas feature</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="8" column="0">
<widget class="QTreeView" name="mLayersTreeView">
<property name="headerHidden">
<bool>true</bool>
</property>
<attribute name="headerDefaultSectionSize">
<number>26</number>
</attribute>
</widget>
</item>
<item row="2" column="0">
<widget class="QComboBox" name="mAtlasClippingTypeComboBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>The clipping mode determines how features from vector layers will be clipped.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="mClipToAtlasLabel">
<property name="text">
<string>&lt;b&gt;When enabled, map layers will be automatically clipped to the boundary of the current atlas feature.&lt;/b&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QRadioButton" name="mRadioClipSelectedLayers">
<property name="text">
<string>Clip selected layers:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QRadioButton" name="mRadioClipAllLayers">
<property name="text">
<string>Clip all layers</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="mForceLabelsInsideCheckBox">
<property name="text">
<string>Force labels inside atlas feature</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>QgsCollapsibleGroupBox</class>
<extends>QGroupBox</extends>
<header>qgscollapsiblegroupbox.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -73,6 +73,7 @@
<addaction name="mActionMoveContent"/>
<addaction name="separator"/>
<addaction name="mActionLabelSettings"/>
<addaction name="mActionClipSettings"/>
</widget>
</item>
<item>
@ -88,8 +89,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>545</width>
<height>1113</height>
<width>548</width>
<height>1284</height>
</rect>
</property>
<property name="sizePolicy">
@ -174,7 +175,7 @@
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QgsProjectionSelectionWidget" name="mCrsSelector">
<widget class="QgsProjectionSelectionWidget" name="mCrsSelector" native="true">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
@ -997,18 +998,59 @@
<string>Labeling Settings</string>
</property>
</action>
<action name="mActionClipSettings">
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/algorithms/mAlgorithmClip.svg</normaloff>:/images/themes/default/algorithms/mAlgorithmClip.svg</iconset>
</property>
<property name="text">
<string>Clipping Settings</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>QgsScrollArea</class>
<extends>QScrollArea</extends>
<header>qgsscrollarea.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>qgsdoublespinbox.h</header>
</customwidget>
<customwidget>
<class>QgsSpinBox</class>
<extends>QSpinBox</extends>
<header>qgsspinbox.h</header>
</customwidget>
<customwidget>
<class>QgsCollapsibleGroupBoxBasic</class>
<extends>QGroupBox</extends>
<header location="global">qgscollapsiblegroupbox.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsLayoutItemComboBox</class>
<extends>QComboBox</extends>
<header>qgslayoutitemcombobox.h</header>
</customwidget>
<customwidget>
<class>QgsPropertyOverrideButton</class>
<extends>QToolButton</extends>
<header>qgspropertyoverridebutton.h</header>
</customwidget>
<customwidget>
<class>QgsMapLayerComboBox</class>
<extends>QComboBox</extends>
<header>qgsmaplayercombobox.h</header>
<header location="global">qgsmaplayercombobox.h</header>
</customwidget>
<customwidget>
<class>QgsBlendModeComboBox</class>
<extends>QComboBox</extends>
<header>qgsblendmodecombobox.h</header>
</customwidget>
<customwidget>
<class>QgsProjectionSelectionWidget</class>
@ -1016,43 +1058,11 @@
<header>qgsprojectionselectionwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsPropertyOverrideButton</class>
<extends>QToolButton</extends>
<header>qgspropertyoverridebutton.h</header>
</customwidget>
<customwidget>
<class>QgsSpinBox</class>
<extends>QSpinBox</extends>
<header>qgsspinbox.h</header>
</customwidget>
<customwidget>
<class>QgsSymbolButton</class>
<extends>QToolButton</extends>
<header>qgssymbolbutton.h</header>
</customwidget>
<customwidget>
<class>QgsScrollArea</class>
<extends>QScrollArea</extends>
<header>qgsscrollarea.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsCollapsibleGroupBoxBasic</class>
<extends>QGroupBox</extends>
<header>qgscollapsiblegroupbox.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsBlendModeComboBox</class>
<extends>QComboBox</extends>
<header>qgsblendmodecombobox.h</header>
</customwidget>
<customwidget>
<class>QgsLayoutItemComboBox</class>
<extends>QComboBox</extends>
<header>qgslayoutitemcombobox.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>scrollArea</tabstop>
@ -1110,6 +1120,8 @@
</tabstops>
<resources>
<include location="../../../images/images.qrc"/>
<include location="../../../images/images.qrc"/>
<include location="../../../images/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -125,6 +125,7 @@ ADD_PYTHON_TEST(PyQgsLayoutItemPropertiesDialog test_qgslayoutitempropertiesdial
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(PyQgsLayoutMapGrid test_qgslayoutmapgrid.py)
ADD_PYTHON_TEST(PyQgsLayoutMapOverview test_qgslayoutmapoverview.py)
ADD_PYTHON_TEST(PyQgsLayoutMarker test_qgslayoutmarker.py)

View File

@ -516,6 +516,62 @@ class TestQgsLayoutAtlas(unittest.TestCase):
self.assertTrue(myTestResult, myMessage)
self.atlas.endRender()
def test_clipping(self):
vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp")
vectorLayer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr")
p = QgsProject()
p.addMapLayers([vectorLayer])
# create layout with layout map
# select epsg:2154
crs = QgsCoordinateReferenceSystem('epsg:2154')
p.setCrs(crs)
layout = QgsPrintLayout(p)
layout.initializeDefaults()
# fix the renderer, fill with green
props = {"color": "0,127,0", 'outline_style': 'no'}
fillSymbol = QgsFillSymbol.createSimple(props)
renderer = QgsSingleSymbolRenderer(fillSymbol)
vectorLayer.setRenderer(renderer)
# the atlas map
atlas_map = QgsLayoutItemMap(layout)
atlas_map.attemptSetSceneRect(QRectF(20, 20, 130, 130))
atlas_map.setFrameEnabled(False)
atlas_map.setLayers([vectorLayer])
layout.addLayoutItem(atlas_map)
# the atlas
atlas = layout.atlas()
atlas.setCoverageLayer(vectorLayer)
atlas.setEnabled(True)
atlas_map.setExtent(
QgsRectangle(332719.06221504929, 6765214.5887386119, 560957.85090677091, 6993453.3774303338))
atlas_map.setAtlasDriven(True)
atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Auto)
atlas_map.setAtlasMargin(0.10)
atlas_map.atlasClippingSettings().setEnabled(True)
atlas.beginRender()
for i in range(0, 2):
atlas.seekTo(i)
checker = QgsLayoutChecker('atlas_clipping%d' % (i + 1), layout)
checker.setControlPathPrefix("atlas")
myTestResult, myMessage = checker.testLayout(0, 200)
self.report += checker.report()
self.assertTrue(myTestResult, myMessage)
atlas.endRender()
def legend_test(self):
self.atlas_map.setAtlasDriven(True)
self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Auto)

View File

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsLayoutItemMapAtlasClippingSettings.
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
"""
__author__ = '(C) 2020 Nyall Dawson'
__date__ = '03/07/2020'
__copyright__ = 'Copyright 2020, The QGIS Project'
import qgis # NOQA
from qgis.core import (QgsLayoutItemMap,
QgsLayout,
QgsProject,
QgsLayoutItemMapAtlasClippingSettings,
QgsMapClippingRegion,
QgsVectorLayer,
QgsReadWriteContext)
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath
from qgis.PyQt.QtTest import QSignalSpy
from qgis.PyQt.QtXml import QDomDocument
start_app()
TEST_DATA_DIR = unitTestDataPath()
class TestQgsLayoutItemMapAtlasClippingSettings(unittest.TestCase):
def testSettings(self):
p = QgsProject()
l = QgsLayout(p)
map = QgsLayoutItemMap(l)
settings = QgsLayoutItemMapAtlasClippingSettings(map)
spy = QSignalSpy(settings.changed)
self.assertFalse(settings.enabled())
settings.setEnabled(True)
self.assertTrue(settings.enabled())
self.assertEqual(len(spy), 1)
settings.setEnabled(True)
self.assertEqual(len(spy), 1)
settings.setFeatureClippingType(QgsMapClippingRegion.FeatureClippingType.NoClipping)
self.assertEqual(settings.featureClippingType(), QgsMapClippingRegion.FeatureClippingType.NoClipping)
self.assertEqual(len(spy), 2)
settings.setFeatureClippingType(QgsMapClippingRegion.FeatureClippingType.NoClipping)
self.assertEqual(len(spy), 2)
self.assertFalse(settings.forceLabelsInsideFeature())
settings.setForceLabelsInsideFeature(True)
self.assertTrue(settings.forceLabelsInsideFeature())
self.assertEqual(len(spy), 3)
settings.setForceLabelsInsideFeature(True)
self.assertEqual(len(spy), 3)
l1 = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
"addfeat", "memory")
l2 = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
"addfeat", "memory")
p.addMapLayers([l1, l2])
self.assertFalse(settings.layersToClip())
settings.setLayersToClip([l1, l2])
self.assertCountEqual(settings.layersToClip(), [l1, l2])
self.assertEqual(len(spy), 4)
p.removeMapLayer(l1.id())
self.assertCountEqual(settings.layersToClip(), [l2])
def testSaveRestore(self):
p = QgsProject()
l1 = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
"addfeat", "memory")
l2 = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
"addfeat", "memory")
p.addMapLayers([l1, l2])
l = QgsLayout(p)
map = QgsLayoutItemMap(l)
settings = map.atlasClippingSettings()
settings.setEnabled(True)
settings.setFeatureClippingType(QgsMapClippingRegion.FeatureClippingType.NoClipping)
settings.setForceLabelsInsideFeature(True)
settings.setLayersToClip([l2])
# save map to xml
doc = QDomDocument("testdoc")
elem = doc.createElement("test")
self.assertTrue(map.writeXml(elem, doc, QgsReadWriteContext()))
layout2 = QgsLayout(p)
map2 = QgsLayoutItemMap(layout2)
self.assertFalse(map2.atlasClippingSettings().enabled())
# restore from xml
self.assertTrue(map2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
self.assertTrue(map2.atlasClippingSettings().enabled())
self.assertEqual(map2.atlasClippingSettings().featureClippingType(), QgsMapClippingRegion.FeatureClippingType.NoClipping)
self.assertTrue(map2.atlasClippingSettings().forceLabelsInsideFeature())
self.assertEqual(map2.atlasClippingSettings().layersToClip(), [l2])
if __name__ == '__main__':
unittest.main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB