[layouts][api] Add API to allow map items to be clipped (take their shape from)

other layout items which provide clipping paths

Opens the door for non-rectangular layout maps
This commit is contained in:
Nyall Dawson 2020-07-27 17:13:45 +10:00
parent 77badc0097
commit 915615aec4
3 changed files with 492 additions and 7 deletions

View File

@ -113,6 +113,137 @@ Emitted when the atlas clipping settings are changed.
};
class QgsLayoutItemMapItemClipPathSettings : QObject
{
%Docstring
Contains settings relating to clipping a layout map by another layout item.
.. versionadded:: 3.16
%End
%TypeHeaderCode
#include "qgslayoutitemmap.h"
%End
public:
QgsLayoutItemMapItemClipPathSettings( QgsLayoutItemMap *map /TransferThis/ = 0 );
%Docstring
Constructor for QgsLayoutItemMapItemClipPathSettings, with the specified ``map`` parent.
%End
bool isActive() const;
%Docstring
Returns ``True`` if the item clipping is enabled and set to a valid source item.
.. seealso:: :py:func:`enabled`
.. seealso:: :py:func:`sourceItem`
%End
bool enabled() const;
%Docstring
Returns ``True`` if the map content should be clipped to the associated item.
.. seealso:: :py:func:`setEnabled`
%End
void setEnabled( bool enabled );
%Docstring
Sets whether the map content should be clipped to the associated item.
.. seealso:: :py:func:`enabled`
%End
QgsGeometry clippedMapExtent() const;
%Docstring
Returns the geometry to use for clipping the parent map, in the map item's CRS.
%End
QgsMapClippingRegion toMapClippingRegion() const;
%Docstring
Returns the clip path as a map clipping region.
%End
void setSourceItem( QgsLayoutItem *item );
%Docstring
Sets the source ``item`` which will provide the clipping path for the map.
The specified ``item`` must return the QgsLayoutItem.FlagProvidesClipPath flag.
.. seealso:: :py:func:`sourceItem`
%End
QgsLayoutItem *sourceItem();
%Docstring
Returns the source item which will provide the clipping path for the map, or ``None``
if no item is set.
.. seealso:: :py:func:`setSourceItem`
%End
QgsMapClippingRegion::FeatureClippingType featureClippingType() const;
%Docstring
Returns the feature clipping type to apply when clipping to the associated item.
.. seealso:: :py:func:`setFeatureClippingType`
%End
void setFeatureClippingType( QgsMapClippingRegion::FeatureClippingType type );
%Docstring
Sets the feature clipping ``type`` to apply when clipping to the associated item.
.. seealso:: :py:func:`featureClippingType`
%End
bool forceLabelsInsideFeature() const;
%Docstring
Returns ``True`` if labels should only be placed inside the clip path geometry.
.. seealso:: :py:func:`setForceLabelsInsideFeature`
%End
void setForceLabelsInsideFeature( bool forceInside );
%Docstring
Sets whether labels should only be placed inside the clip path geometry.
.. seealso:: :py:func:`forceLabelsInsideFeature`
%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`
.. seealso:: :py:func:`finalizeRestoreFromXml`
%End
void finalizeRestoreFromXml();
%Docstring
To be called after all pending items have been restored from XML.
.. seealso:: :py:func:`readXml`
%End
signals:
void changed();
%Docstring
Emitted when the item clipping settings are changed.
%End
};
class QgsLayoutItemMap : QgsLayoutItem, QgsTemporalRangeObject
{
%Docstring
@ -731,6 +862,13 @@ Creates a transform from layout coordinates to map coordinates.
%Docstring
Returns the map's atlas clipping settings.
.. versionadded:: 3.16
%End
QgsLayoutItemMapItemClipPathSettings *itemClippingSettings();
%Docstring
Returns the map's item based clip path settings.
.. versionadded:: 3.16
%End

View File

@ -40,6 +40,7 @@
QgsLayoutItemMap::QgsLayoutItemMap( QgsLayout *layout )
: QgsLayoutItem( layout )
, mAtlasClippingSettings( new QgsLayoutItemMapAtlasClippingSettings( this ) )
, mItemClippingSettings( new QgsLayoutItemMapItemClipPathSettings( this ) )
{
mBackgroundUpdateTimer = new QTimer( this );
mBackgroundUpdateTimer->setSingleShot( true );
@ -62,6 +63,11 @@ QgsLayoutItemMap::QgsLayoutItemMap( QgsLayout *layout )
refresh();
} );
connect( mItemClippingSettings, &QgsLayoutItemMapItemClipPathSettings::changed, this, [ = ]
{
refresh();
} );
if ( layout )
connectUpdateSlot();
}
@ -260,13 +266,28 @@ QgsRectangle QgsLayoutItemMap::extent() const
return mExtent;
}
QPolygonF QgsLayoutItemMap::visibleExtentPolygon() const
QPolygonF QgsLayoutItemMap::calculateVisibleExtentPolygon( bool includeClipping ) const
{
QPolygonF poly;
mapPolygon( mExtent, poly );
if ( includeClipping && mItemClippingSettings->isActive() )
{
const QgsGeometry geom = mItemClippingSettings->clippedMapExtent();
if ( !geom.isEmpty() )
{
poly = poly.intersected( geom.asQPolygonF() );
}
}
return poly;
}
QPolygonF QgsLayoutItemMap::visibleExtentPolygon() const
{
return calculateVisibleExtentPolygon( true );
}
QgsCoordinateReferenceSystem QgsLayoutItemMap::crs() const
{
if ( mCrs.isValid() )
@ -664,6 +685,7 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &mapElem, QDomDocum
}
mAtlasClippingSettings->writeXml( mapElem, doc, context );
mItemClippingSettings->writeXml( mapElem, doc, context );
return true;
}
@ -820,6 +842,7 @@ bool QgsLayoutItemMap::readPropertiesFromElement( const QDomElement &itemElem, c
}
mAtlasClippingSettings->readXml( itemElem, doc, context );
mItemClippingSettings->readXml( itemElem, doc, context );
updateBoundingRect();
@ -1463,6 +1486,7 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF
// override the default text render format inherited from the labeling engine settings using the layout's render context setting
jobMapSettings.setTextRenderFormat( mLayout->renderContext().textRenderFormat() );
QgsGeometry labelBoundary;
if ( mEvaluatedLabelMargin.length() > 0 )
{
QPolygonF visiblePoly = jobMapSettings.visiblePolygon();
@ -1471,7 +1495,7 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF
const double layoutLabelMarginInMapUnits = layoutLabelMargin / rect().width() * jobMapSettings.extent().width();
QgsGeometry mapBoundaryGeom = QgsGeometry::fromQPolygonF( visiblePoly );
mapBoundaryGeom = mapBoundaryGeom.buffer( -layoutLabelMarginInMapUnits, 0 );
jobMapSettings.setLabelBoundaryGeometry( mapBoundaryGeom );
labelBoundary = mapBoundaryGeom;
}
if ( !mBlockingLabelItems.isEmpty() )
@ -1497,13 +1521,44 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF
if ( mAtlasClippingSettings->forceLabelsInsideFeature() )
{
if ( !jobMapSettings.labelBoundaryGeometry().isEmpty() )
if ( !labelBoundary.isEmpty() )
{
clipGeom = clipGeom.intersection( jobMapSettings.labelBoundaryGeometry() );
labelBoundary = clipGeom.intersection( labelBoundary );
}
jobMapSettings.setLabelBoundaryGeometry( clipGeom );
else
{
labelBoundary = clipGeom;
}
}
}
if ( mItemClippingSettings->isActive() )
{
const QgsGeometry clipGeom = mItemClippingSettings->clippedMapExtent();
if ( !clipGeom.isEmpty() )
{
jobMapSettings.addClippingRegion( mItemClippingSettings->toMapClippingRegion() );
if ( mItemClippingSettings->forceLabelsInsideFeature() )
{
const double layoutLabelMargin = mLayout->convertToLayoutUnits( mEvaluatedLabelMargin );
const double layoutLabelMarginInMapUnits = layoutLabelMargin / rect().width() * jobMapSettings.extent().width();
QgsGeometry mapBoundaryGeom = clipGeom;
mapBoundaryGeom = mapBoundaryGeom.buffer( -layoutLabelMarginInMapUnits, 0 );
if ( !labelBoundary.isEmpty() )
{
labelBoundary = mapBoundaryGeom.intersection( labelBoundary );
}
else
{
labelBoundary = mapBoundaryGeom;
}
}
}
}
if ( !labelBoundary.isNull() )
jobMapSettings.setLabelBoundaryGeometry( labelBoundary );
return jobMapSettings;
}
@ -1524,6 +1579,7 @@ void QgsLayoutItemMap::finalizeRestoreFromXml()
mOverviewStack->finalizeRestoreFromXml();
mGridStack->finalizeRestoreFromXml();
mItemClippingSettings->finalizeRestoreFromXml();
}
void QgsLayoutItemMap::setMoveContentPreviewOffset( double xOffset, double yOffset )
@ -1613,7 +1669,7 @@ QPolygonF QgsLayoutItemMap::transformedMapPolygon() const
double dx = mXOffset;
double dy = mYOffset;
transformShift( dx, dy );
QPolygonF poly = visibleExtentPolygon();
QPolygonF poly = calculateVisibleExtentPolygon( false );
poly.translate( -dx, -dy );
return poly;
}
@ -1922,7 +1978,7 @@ void QgsLayoutItemMap::connectUpdateSlot()
QTransform QgsLayoutItemMap::layoutToMapCoordsTransform() const
{
QPolygonF thisExtent = visibleExtentPolygon();
QPolygonF thisExtent = calculateVisibleExtentPolygon( false );
QTransform mapTransform;
QPolygonF thisRectPoly = QPolygonF( QRectF( 0, 0, rect().width(), rect().height() ) );
//workaround QT Bug #21329
@ -2633,6 +2689,7 @@ void QgsLayoutItemMap::createStagedRenderJob( const QgsRectangle &extent, const
}
//
// QgsLayoutItemMapAtlasClippingSettings
//
@ -2770,3 +2827,148 @@ void QgsLayoutItemMapAtlasClippingSettings::layersAboutToBeRemoved( const QList<
_qgis_removeLayers( mLayersToClip, layers );
}
}
//
// QgsLayoutItemMapItemClipPathSettings
//
QgsLayoutItemMapItemClipPathSettings::QgsLayoutItemMapItemClipPathSettings( QgsLayoutItemMap *map )
: QObject( map )
, mMap( map )
{
}
bool QgsLayoutItemMapItemClipPathSettings::isActive() const
{
return mEnabled && mClipPathSource;
}
bool QgsLayoutItemMapItemClipPathSettings::enabled() const
{
return mEnabled;
}
void QgsLayoutItemMapItemClipPathSettings::setEnabled( bool enabled )
{
if ( enabled == mEnabled )
return;
mEnabled = enabled;
emit changed();
}
QgsGeometry QgsLayoutItemMapItemClipPathSettings::clippedMapExtent() const
{
if ( isActive() )
{
QgsGeometry clipGeom( mClipPathSource->clipPath() );
clipGeom.transform( mMap->layoutToMapCoordsTransform() );
return clipGeom;
}
return QgsGeometry();
}
QgsMapClippingRegion QgsLayoutItemMapItemClipPathSettings::toMapClippingRegion() const
{
QgsMapClippingRegion region( clippedMapExtent() );
region.setFeatureClip( mFeatureClippingType );
return region;
}
void QgsLayoutItemMapItemClipPathSettings::setSourceItem( QgsLayoutItem *item )
{
if ( mClipPathSource == item )
return;
if ( mClipPathSource )
{
disconnect( mClipPathSource, &QgsLayoutItem::sizePositionChanged, mMap, &QgsLayoutItemMap::refresh );
disconnect( mClipPathSource, &QgsLayoutItem::rotationChanged, mMap, &QgsLayoutItemMap::refresh );
disconnect( mClipPathSource, &QgsLayoutItem::sizePositionChanged, mMap, &QgsLayoutItemMap::extentChanged );
disconnect( mClipPathSource, &QgsLayoutItem::rotationChanged, mMap, &QgsLayoutItemMap::extentChanged );
}
mClipPathSource = item;
if ( mClipPathSource )
{
// if item size or rotation changes, we need to redraw this map
connect( mClipPathSource, &QgsLayoutItem::sizePositionChanged, mMap, &QgsLayoutItemMap::refresh );
connect( mClipPathSource, &QgsLayoutItem::rotationChanged, mMap, &QgsLayoutItemMap::refresh );
// and if clip item size or rotation changes, then effectively we've changed the visible extent of the map
connect( mClipPathSource, &QgsLayoutItem::sizePositionChanged, mMap, &QgsLayoutItemMap::extentChanged );
connect( mClipPathSource, &QgsLayoutItem::rotationChanged, mMap, &QgsLayoutItemMap::extentChanged );
}
emit changed();
}
QgsLayoutItem *QgsLayoutItemMapItemClipPathSettings::sourceItem()
{
return mClipPathSource;
}
QgsMapClippingRegion::FeatureClippingType QgsLayoutItemMapItemClipPathSettings::featureClippingType() const
{
return mFeatureClippingType;
}
void QgsLayoutItemMapItemClipPathSettings::setFeatureClippingType( QgsMapClippingRegion::FeatureClippingType type )
{
if ( mFeatureClippingType == type )
return;
mFeatureClippingType = type;
emit changed();
}
bool QgsLayoutItemMapItemClipPathSettings::forceLabelsInsideFeature() const
{
return mForceLabelsInsideClipPath;
}
void QgsLayoutItemMapItemClipPathSettings::setForceLabelsInsideFeature( bool forceInside )
{
if ( forceInside == mForceLabelsInsideClipPath )
return;
mForceLabelsInsideClipPath = forceInside;
emit changed();
}
bool QgsLayoutItemMapItemClipPathSettings::writeXml( QDomElement &element, QDomDocument &document, const QgsReadWriteContext & ) const
{
QDomElement settingsElem = document.createElement( QStringLiteral( "itemClippingSettings" ) );
settingsElem.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
settingsElem.setAttribute( QStringLiteral( "forceLabelsInside" ), mForceLabelsInsideClipPath ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
settingsElem.setAttribute( QStringLiteral( "clippingType" ), QString::number( static_cast<int>( mFeatureClippingType ) ) );
if ( mClipPathSource )
settingsElem.setAttribute( QStringLiteral( "clipSource" ), mClipPathSource->uuid() );
else
settingsElem.setAttribute( QStringLiteral( "clipSource" ), QString() );
element.appendChild( settingsElem );
return true;
}
bool QgsLayoutItemMapItemClipPathSettings::readXml( const QDomElement &element, const QDomDocument &, const QgsReadWriteContext & )
{
const QDomElement settingsElem = element.firstChildElement( QStringLiteral( "itemClippingSettings" ) );
mEnabled = settingsElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt();
mForceLabelsInsideClipPath = settingsElem.attribute( QStringLiteral( "forceLabelsInside" ), QStringLiteral( "0" ) ).toInt();
mFeatureClippingType = static_cast< QgsMapClippingRegion::FeatureClippingType >( settingsElem.attribute( QStringLiteral( "clippingType" ), QStringLiteral( "0" ) ).toInt() );
mClipPathUuid = settingsElem.attribute( QStringLiteral( "clipSource" ) );
return true;
}
void QgsLayoutItemMapItemClipPathSettings::finalizeRestoreFromXml()
{
if ( !mClipPathUuid.isEmpty() )
{
if ( QgsLayoutItem *item = mMap->layout()->itemByUuid( mClipPathUuid, true ) )
{
setSourceItem( item );
}
}
}

View File

@ -141,6 +141,141 @@ class CORE_EXPORT QgsLayoutItemMapAtlasClippingSettings : public QObject
};
/**
* \ingroup core
* \class QgsLayoutItemMapItemClipPathSettings
* \brief Contains settings relating to clipping a layout map by another layout item.
* \since QGIS 3.16
*/
class CORE_EXPORT QgsLayoutItemMapItemClipPathSettings : public QObject
{
Q_OBJECT
public:
/**
* Constructor for QgsLayoutItemMapItemClipPathSettings, with the specified \a map parent.
*/
QgsLayoutItemMapItemClipPathSettings( QgsLayoutItemMap *map SIP_TRANSFERTHIS = nullptr );
/**
* Returns TRUE if the item clipping is enabled and set to a valid source item.
*
* \see enabled()
* \see sourceItem()
*/
bool isActive() const;
/**
* Returns TRUE if the map content should be clipped to the associated item.
*
* \see setEnabled()
*/
bool enabled() const;
/**
* Sets whether the map content should be clipped to the associated item.
*
* \see enabled()
*/
void setEnabled( bool enabled );
/**
* Returns the geometry to use for clipping the parent map, in the map item's CRS.
*/
QgsGeometry clippedMapExtent() const;
/**
* Returns the clip path as a map clipping region.
*/
QgsMapClippingRegion toMapClippingRegion() const;
/**
* Sets the source \a item which will provide the clipping path for the map.
*
* The specified \a item must return the QgsLayoutItem::FlagProvidesClipPath flag.
*
* \see sourceItem()
*/
void setSourceItem( QgsLayoutItem *item );
/**
* Returns the source item which will provide the clipping path for the map, or NULLPTR
* if no item is set.
*
* \see setSourceItem()
*/
QgsLayoutItem *sourceItem();
/**
* Returns the feature clipping type to apply when clipping to the associated item.
*
* \see setFeatureClippingType()
*/
QgsMapClippingRegion::FeatureClippingType featureClippingType() const;
/**
* Sets the feature clipping \a type to apply when clipping to the associated item.
*
* \see featureClippingType()
*/
void setFeatureClippingType( QgsMapClippingRegion::FeatureClippingType type );
/**
* Returns TRUE if labels should only be placed inside the clip path geometry.
*
* \see setForceLabelsInsideFeature()
*/
bool forceLabelsInsideFeature() const;
/**
* Sets whether labels should only be placed inside the clip path geometry.
*
* \see forceLabelsInsideFeature()
*/
void setForceLabelsInsideFeature( bool forceInside );
/**
* 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()
* \see finalizeRestoreFromXml()
*/
bool readXml( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context );
/**
* To be called after all pending items have been restored from XML.
* \see readXml()
*/
void finalizeRestoreFromXml();
signals:
/**
* Emitted when the item clipping settings are changed.
*/
void changed();
private:
QgsLayoutItemMap *mMap = nullptr;
bool mEnabled = false;
QgsMapClippingRegion::FeatureClippingType mFeatureClippingType = QgsMapClippingRegion::FeatureClippingType::ClipPainterOnly;
bool mForceLabelsInsideClipPath = false;
QPointer< QgsLayoutItem > mClipPathSource;
QString mClipPathUuid;
};
/**
* \ingroup core
* \class QgsLayoutItemMap
@ -677,6 +812,13 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem, public QgsTemporalRan
*/
QgsLayoutItemMapAtlasClippingSettings *atlasClippingSettings() { return mAtlasClippingSettings; }
/**
* Returns the map's item based clip path settings.
*
* \since QGIS 3.16
*/
QgsLayoutItemMapItemClipPathSettings *itemClippingSettings() { return mItemClippingSettings; }
protected:
void draw( QgsLayoutItemRenderContext &context ) override;
@ -962,6 +1104,7 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem, public QgsTemporalRan
QStringList::iterator mExportThemeIt;
QgsLayoutItemMapAtlasClippingSettings *mAtlasClippingSettings = nullptr;
QgsLayoutItemMapItemClipPathSettings *mItemClippingSettings = nullptr;
/**
* Refresh the map's extents, considering data defined extent, scale and rotation
@ -975,6 +1118,8 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem, public QgsTemporalRan
void createStagedRenderJob( const QgsRectangle &extent, const QSizeF size, double dpi );
QPolygonF calculateVisibleExtentPolygon( bool includeClipping ) const;
friend class QgsLayoutItemMapGrid;
friend class QgsLayoutItemMapOverview;
friend class QgsLayoutItemLegend;