Skip to content

Commit

Permalink
[feature][layouts] Add automatic clipping settings for atlas maps
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nyalldawson committed Jul 4, 2020
1 parent 329a0fc commit c30c769
Show file tree
Hide file tree
Showing 15 changed files with 1,001 additions and 33 deletions.
111 changes: 111 additions & 0 deletions python/core/auto_generated/layout/qgslayoutitemmap.sip.in
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions python/core/auto_generated/qgsmaplayermodel.sip.in
Expand Up @@ -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
Expand Down
165 changes: 165 additions & 0 deletions src/core/layout/qgslayoutitemmap.cpp
Expand Up @@ -39,6 +39,7 @@

QgsLayoutItemMap::QgsLayoutItemMap( QgsLayout *layout )
: QgsLayoutItem( layout )
, mAtlasClippingSettings( new QgsLayoutItemMapAtlasClippingSettings( this ) )
{
mBackgroundUpdateTimer = new QTimer( this );
mBackgroundUpdateTimer->setSingleShot( true );
Expand All @@ -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();
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -811,6 +819,8 @@ bool QgsLayoutItemMap::readPropertiesFromElement( const QDomElement &itemElem, c
}
}

mAtlasClippingSettings->readXml( itemElem, doc, context );

updateBoundingRect();

//temporal settings
Expand Down Expand Up @@ -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;
}

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

0 comments on commit c30c769

Please sign in to comment.