Symbol aware legend expression (#9648)

This commit is contained in:
Alex 2019-07-15 01:12:24 -04:00 committed by Matthias Kuhn
parent eb73702982
commit 248af94ba9
24 changed files with 577 additions and 63 deletions

View File

@ -177,6 +177,7 @@
<file>themes/default/mActionDistributeTop.svg</file>
<file>themes/default/mActionDistributeVCenter.svg</file>
<file>themes/default/mActionDistributeVSpace.svg</file>
<file>themes/default/mActionAddExpression.svg</file>
<file>themes/default/mActionAddLayer.svg</file>
<file>themes/default/mActionAddMeshLayer.svg</file>
<file>themes/default/mActionAddAllToOverview.svg</file>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14.7967" height="16.7515" viewBox="0 0 3.915 4.4322"><path d="M1.1438963 1.910687c-.5300386-.1812338-.7956598-.455994-.7956598-.8236459 0-.2899952.1400886-.521801.4198282-.695523C1.0504308.218325 1.4006524.1314639 1.817635.1314639c.3786771 0 .6791016.0610461.9025868.1822918.2238134.1193414.335556.263334.335556.4315545 0 .0880248-.034584.1652581-.1050665.2330753-.069606.0646433-.1507047.0971236-.2419812.0971236-.151033 0-.274705-.1011439-.3716727-.30322C2.203426.492662 2.002158.3527955 1.7336913.3527955c-.2125407 0-.3879799.0670766-.5252229.2017589-.1374621.134788-.2063025.3226872-.2063025.5635917 0 .4736625.2550052.710864.7638115.710864.053518 0 .1150259-.00529.1846324-.015658.1209359-.015235.2148391-.02317.2820378-.02317.1634003 0 .2457024.045176.2457024.135846 0 .1009323-.083506.1512927-.2501896.1512927-.05899 0-.1472025-.0091-.2652928-.02719-.088869-.015658-.1572714-.023382-.2054269-.023382-.538794 0-.807808.2630165-.807808.7903191 0 .2561396.071467.4634.2142919.6214638.1424964.155313.342451.2331811.5975655.2331811.3195773 0 .5308047-.1597566.6358711-.4782118.054065-.1681147.1130559-.2849169.177628-.3497717.067746-.064326.1565053-.097018.2671535-.097018.091495 0 .1729219.032692.2462495.097018.075516.06221.1129465.1426171.1129465.2409045 0 .2358261-.137462.431237-.4119482.587079-.2750334.1523507-.6067589.2287376-.9960521.2287376-.4274892 0-.808793-.098181-1.1420508-.2949678-.3310688-.197104-.4968768-.4602263-.4968768-.7887324 0-.4108178.3299744-.7124509.9894855-.9060632" fill="#5c3566"/><g transform="matrix(.13805 0 0 .13357 -.709128 -.0487503)"><rect y="19" x="19" width="13" ry="2.6149001" rx="2.6149001" height="13" fill="#5a8c5a"/><path d="M21.6 25.5h7.8m-3.9 3.9v-7.8" overflow="visible" fill="#fff" fill-rule="evenodd" stroke="#fff" stroke-width="2.5999999" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.3 25.5h10.4v-2.6c0-2.6-.65-2.6-5.2-2.6s-5.2 0-5.2 2.6z" opacity=".3" fill="#fcffff" fill-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -122,6 +122,20 @@ Also resolves textual references to layers from the project (calls resolveRefere
Resolves reference to layer from stored layer ID (if it has not been resolved already)
.. versionadded:: 3.0
%End
void setLabelExpression( const QString &expression );
%Docstring
set the expression to evaluate
.. versionadded:: 3.10
%End
QString labelExpression() const;
%Docstring
Returns the expression member of the LayerTreeNode
.. versionadded:: 3.10
%End
signals:

View File

@ -12,6 +12,7 @@
class QgsLayerTreeModelLegendNode : QObject
{
%Docstring
@ -26,6 +27,12 @@ and customized look.
%TypeHeaderCode
#include "qgslayertreemodellegendnode.h"
%End
%ConvertToSubClassCode
if ( qobject_cast<QgsSymbolLegendNode *> ( sipCpp ) )
sipType = sipType_QgsSymbolLegendNode;
else
sipType = 0;
%End
public:
@ -309,6 +316,23 @@ Returns text format of the label to be shown on top of the symbol.
Sets format of text to be shown on top of the symbol.
.. versionadded:: 3.2
%End
QString symbolLabel() const;
%Docstring
Label of the symbol, user defined label will be used, otherwise will default to the label made by QGIS.
.. versionadded:: 3.10
%End
QString evaluateLabel( const QgsExpressionContext &context = QgsExpressionContext(), const QString &label = QString() );
%Docstring
Evaluates and returns the text label of the current node
:param context: extra QgsExpressionContext to use for evaluating the expression
:param label: text to evaluate instead of the layer layertree string
.. versionadded:: 3.10
%End
public slots:

View File

@ -24,9 +24,14 @@ Overrides some functionality of QgsLayerTreeModel to better fit the needs of lay
#include "qgslayoutitemlegend.h"
%End
public:
QgsLegendModel( QgsLayerTree *rootNode, QObject *parent /TransferThis/ = 0 );
QgsLegendModel( QgsLayerTree *rootNode, QObject *parent /TransferThis/ = 0, QgsLayoutItemLegend *layout = 0 );
%Docstring
Construct the model based on the given layer tree
%End
QgsLegendModel( QgsLayerTree *rootNode, QgsLayoutItemLegend *layout );
%Docstring
Alternative constructor.
%End
virtual QVariant data( const QModelIndex &index, int role ) const;
@ -34,6 +39,16 @@ Construct the model based on the given layer tree
virtual Qt::ItemFlags flags( const QModelIndex &index ) const;
signals:
void refreshLegend();
%Docstring
Emitted to refresh the legend.
.. versionadded:: 3.10
%End
};
@ -513,7 +528,6 @@ Returns the legend's renderer settings object.
virtual QgsExpressionContext createExpressionContext() const;
public slots:
virtual void refresh();

View File

@ -2279,7 +2279,6 @@ Configuration and logic to apply automatically on any edit happening on this lay
public slots:
void select( QgsFeatureId featureId );
@ -2653,6 +2652,7 @@ Emitted when the feature count for symbols on this layer has been recalculated.
.. versionadded:: 3.0
%End
protected:
virtual void setExtent( const QgsRectangle &rect ) ${SIP_FINAL};

View File

@ -29,16 +29,29 @@ QgsVectorLayer.countSymbolFeatures() and connect to the signal
Create a new feature counter for ``layer``.
%End
virtual bool run();
%Docstring
Calculates the feature count and Ids per symbol
%End
long featureCount( const QString &legendKey ) const;
%Docstring
Gets the feature count for a particular ``legendKey``.
Returns the feature count for a particular ``legendKey``.
If the key has not been found, -1 will be returned.
%End
QgsFeatureIds featureIds( const QString &symbolkey ) const;
%Docstring
Returns the feature Ids for a particular ``legendKey``.
If the key has not been found an empty QSet will be returned.
.. versionadded:: 3.10
%End
signals:
void symbolsCounted();

View File

@ -38,6 +38,7 @@
#include "qgslayoutitemlegend.h"
#include "qgslayoutmeasurementconverter.h"
#include "qgsunittypes.h"
#include "qgsexpressionbuilderdialog.h"
#include <QMessageBox>
#include <QInputDialog>
@ -104,6 +105,7 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend )
connect( mEditPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mEditPushButton_clicked );
connect( mCountToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mCountToolButton_clicked );
connect( mExpressionFilterButton, &QgsLegendFilterButton::toggled, this, &QgsLayoutLegendWidget::mExpressionFilterButton_toggled );
connect( mLayerExpressionButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mLayerExpressionButton_clicked );
connect( mFilterByMapToolButton, &QToolButton::toggled, this, &QgsLayoutLegendWidget::mFilterByMapToolButton_toggled );
connect( mUpdateAllPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mUpdateAllPushButton_clicked );
connect( mAddGroupToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mAddGroupToolButton_clicked );
@ -136,6 +138,7 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend )
mMoveUpToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowUp.svg" ) ) );
mMoveDownToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowDown.svg" ) ) );
mCountToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionSum.svg" ) ) );
mLayerExpressionButton->setIcon( QIcon( QgsApplication::iconPath( "mActionAddExpression.svg" ) ) );
mFontColorButton->setColorDialogTitle( tr( "Select Font Color" ) );
mFontColorButton->setContext( QStringLiteral( "composer" ) );
@ -994,6 +997,45 @@ void QgsLayoutLegendWidget::mExpressionFilterButton_toggled( bool checked )
mLegend->endCommand();
}
void QgsLayoutLegendWidget::mLayerExpressionButton_clicked()
{
if ( !mLegend )
{
return;
}
QModelIndex currentIndex = mItemTreeView->currentIndex();
if ( !currentIndex.isValid() )
return;
QgsLayerTreeNode *currentNode = mItemTreeView->currentNode();
if ( !QgsLayerTree::isLayer( currentNode ) )
return;
QgsLayerTreeLayer *layerNode = qobject_cast<QgsLayerTreeLayer *>( currentNode );
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layerNode->layer() );
if ( !vl )
return;
QString currentExpression;
if ( layerNode->labelExpression().isEmpty() )
currentExpression = QStringLiteral( "@symbol_label" );
else
currentExpression = layerNode->labelExpression();
QgsExpressionContext legendContext = mLegend->createExpressionContext();
legendContext.appendScope( vl->createExpressionContextScope() );
QgsExpressionBuilderDialog expressiondialog( vl, currentExpression, nullptr, "generic", legendContext );
if ( expressiondialog.exec() )
layerNode->setLabelExpression( expressiondialog.expressionText() );
mLegend->beginCommand( tr( "Update Legend" ) );
mLegend->updateLegend();
mLegend->adjustBoxSize();
mLegend->endCommand();
}
void QgsLayoutLegendWidget::mUpdateAllPushButton_clicked()
{
updateLegend();
@ -1105,12 +1147,27 @@ void QgsLayoutLegendWidget::selectedChanged( const QModelIndex &current, const Q
Q_UNUSED( current )
Q_UNUSED( previous )
mLayerExpressionButton->setEnabled( false );
if ( mLegend && mLegend->autoUpdateModel() )
{
QgsLayerTreeNode *currentNode = mItemTreeView->currentNode();
if ( !QgsLayerTree::isLayer( currentNode ) )
return;
QgsLayerTreeLayer *currentLayerNode = QgsLayerTree::toLayer( currentNode );
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( currentLayerNode->layer() );
if ( !vl )
return;
mLayerExpressionButton->setEnabled( true );
return;
}
mCountToolButton->setChecked( false );
mCountToolButton->setEnabled( false );
mExpressionFilterButton->blockSignals( true );
mExpressionFilterButton->setChecked( false );
mExpressionFilterButton->setEnabled( false );
@ -1127,6 +1184,7 @@ void QgsLayoutLegendWidget::selectedChanged( const QModelIndex &current, const Q
mCountToolButton->setChecked( currentNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt() );
mCountToolButton->setEnabled( true );
mLayerExpressionButton->setEnabled( true );
bool exprEnabled;
QString expr = QgsLayerTreeUtils::legendFilterByExpression( *qobject_cast<QgsLayerTreeLayer *>( currentNode ), &exprEnabled );

View File

@ -85,6 +85,7 @@ class QgsLayoutLegendWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayo
void resetLayerNodeToDefaults();
void mUpdateAllPushButton_clicked();
void mAddGroupToolButton_clicked();
void mLayerExpressionButton_clicked();
void mFilterLegendByAtlasCheckBox_toggled( bool checked );

View File

@ -830,6 +830,10 @@ void QgsExpression::initVariableHelp()
sVariableHelpTexts.insert( QStringLiteral( "symbol_color" ), QCoreApplication::translate( "symbol_color", "Color of symbol used to render the feature." ) );
sVariableHelpTexts.insert( QStringLiteral( "symbol_angle" ), QCoreApplication::translate( "symbol_angle", "Angle of symbol used to render the feature (valid for marker symbols only)." ) );
sVariableHelpTexts.insert( QStringLiteral( "symbol_label" ), QCoreApplication::translate( "symbol_label", "Label of the symbol, user defined label will be used, otherwise will default to the label made by QGIS." ) );
sVariableHelpTexts.insert( QStringLiteral( "symbol_id" ), QCoreApplication::translate( "symbol_id", "Id of the symbol." ) );
sVariableHelpTexts.insert( QStringLiteral( "symbol_count" ), QCoreApplication::translate( "symbol_count", "Total number of features defined by this symbol." ) );
//cluster variables
sVariableHelpTexts.insert( QStringLiteral( "cluster_color" ), QCoreApplication::translate( "cluster_color", "Color of symbols within a cluster, or NULL if symbols have mixed colors." ) );
sVariableHelpTexts.insert( QStringLiteral( "cluster_size" ), QCoreApplication::translate( "cluster_size", "Number of symbols contained within a cluster." ) );

View File

@ -188,3 +188,9 @@ void QgsLayerTreeLayer::layerNameChanged()
Q_ASSERT( mRef );
emit nameChanged( this, mRef->name() );
}
void QgsLayerTreeLayer::setLabelExpression( const QString &expression )
{
mLabelExpression = expression;
}

View File

@ -128,6 +128,20 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode
*/
void resolveReferences( const QgsProject *project, bool looseMatching = false ) override;
/**
* set the expression to evaluate
*
* \since QGIS 3.10
*/
void setLabelExpression( const QString &expression );
/**
* Returns the expression member of the LayerTreeNode
*
* \since QGIS 3.10
*/
QString labelExpression() const { return mLabelExpression; }
signals:
/**
@ -148,6 +162,8 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode
QgsMapLayerRef mRef;
//! Layer name - only used if layer does not exist or if mUseLayerName is false
QString mLayerName;
//! Expression to evaluate in the legend
QString mLabelExpression;
//!
bool mUseLayerName = true;

View File

@ -29,6 +29,11 @@
#include "qgsvectorlayer.h"
#include "qgsrasterrenderer.h"
#include "qgsexpressioncontextutils.h"
#include "qgsfeatureid.h"
#include "qgslayoutitem.h"
#include "qgsvectorlayerfeaturecounter.h"
#include "qgsexpression.h"
QgsLayerTreeModelLegendNode::QgsLayerTreeModelLegendNode( QgsLayerTreeLayer *nodeL, QObject *parent )
: QObject( parent )
@ -284,6 +289,20 @@ const QgsSymbol *QgsSymbolLegendNode::symbol() const
return mItem.symbol();
}
QString QgsSymbolLegendNode::symbolLabel() const
{
QString label;
if ( mEmbeddedInParent )
{
QVariant legendlabel = mLayerNode->customProperty( QStringLiteral( "legend/title-label" ) );
QString layerName = legendlabel.isNull() ? mLayerNode->name() : legendlabel.toString();
label = mUserLabel.isEmpty() ? layerName : mUserLabel;
}
else
label = mUserLabel.isEmpty() ? mItem.label() : mUserLabel;
return label;
}
void QgsSymbolLegendNode::setSymbol( QgsSymbol *symbol )
{
if ( !symbol )
@ -648,31 +667,68 @@ void QgsSymbolLegendNode::updateLabel()
bool showFeatureCount = mLayerNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toBool();
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( mLayerNode->layer() );
mLabel = symbolLabel();
if ( mEmbeddedInParent )
if ( showFeatureCount && vl )
{
QString layerName = mLayerNode->name();
if ( !mLayerNode->customProperty( QStringLiteral( "legend/title-label" ) ).isNull() )
layerName = mLayerNode->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
mLabel = mUserLabel.isEmpty() ? layerName : mUserLabel;
if ( showFeatureCount && vl && vl->featureCount() >= 0 )
mLabel += QStringLiteral( " [%1]" ).arg( vl->featureCount() );
}
else
{
mLabel = mUserLabel.isEmpty() ? mItem.label() : mUserLabel;
if ( showFeatureCount && vl )
{
qlonglong count = vl->featureCount( mItem.ruleKey() );
mLabel += QStringLiteral( " [%1]" ).arg( count != -1 ? QLocale().toString( count ) : tr( "N/A" ) );
}
qlonglong count = mEmbeddedInParent ? vl->featureCount() : vl->featureCount( mItem.ruleKey() ) ;
mLabel += QStringLiteral( " [%1]" ).arg( count != -1 ? QLocale().toString( count ) : tr( "N/A" ) );
}
emit dataChanged();
}
QString QgsSymbolLegendNode::evaluateLabel( const QgsExpressionContext &context, const QString &label )
{
if ( !mLayerNode )
return QString();
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( mLayerNode->layer() );
if ( vl )
{
QgsExpressionContext contextCopy = QgsExpressionContext( context );
QgsExpressionContextScope *symbolScope = createSymbolScope();
contextCopy.appendScope( symbolScope );
contextCopy.appendScope( vl->createExpressionContextScope() );
if ( label.isEmpty() )
{
if ( ! mLayerNode->labelExpression().isEmpty() )
mLabel = QgsExpression::replaceExpressionText( "[%" + mLayerNode->labelExpression() + "%]", &contextCopy );
else if ( mLabel.contains( "[%" ) )
{
const QString symLabel = symbolLabel();
mLabel = QgsExpression::replaceExpressionText( symLabel, &contextCopy );
}
return mLabel;
}
else
{
QString eLabel;
if ( ! mLayerNode->labelExpression().isEmpty() )
eLabel = QgsExpression::replaceExpressionText( label + "[%" + mLayerNode->labelExpression() + "%]", &contextCopy );
else if ( label.contains( "[%" ) )
eLabel = QgsExpression::replaceExpressionText( label, &contextCopy );
return eLabel;
}
}
return mLabel;
}
QgsExpressionContextScope *QgsSymbolLegendNode::createSymbolScope() const
{
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( mLayerNode->layer() );
QgsExpressionContextScope *scope = new QgsExpressionContextScope( tr( "Symbol scope" ) );
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_label" ), symbolLabel().remove( "[%" ).remove( "%]" ), true ) );
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_id" ), mItem.ruleKey(), true ) );
if ( vl )
{
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "symbol_count" ), QVariant::fromValue( vl->featureCount( mItem.ruleKey() ) ), true ) );
}
return scope;
}
// -------------------------------------------------------------------------

View File

@ -27,6 +27,7 @@
#include "qgis_sip.h"
#include "qgsrasterdataprovider.h" // for QgsImageFetcher dtor visibility
#include "qgsexpressioncontext.h"
class QgsLayerTreeLayer;
class QgsLayerTreeModel;
@ -48,6 +49,14 @@ class QgsRenderContext;
class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject
{
Q_OBJECT
#ifdef SIP_RUN
SIP_CONVERT_TO_SUBCLASS_CODE
if ( qobject_cast<QgsSymbolLegendNode *> ( sipCpp ) )
sipType = sipType_QgsSymbolLegendNode;
else
sipType = 0;
SIP_END
#endif
public:
enum LegendNodeRoles
@ -228,6 +237,7 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
{
Q_OBJECT
public:
/**
@ -319,6 +329,20 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
*/
void setTextOnSymbolTextFormat( const QgsTextFormat &format ) { mTextOnSymbolTextFormat = format; }
/**
* Label of the symbol, user defined label will be used, otherwise will default to the label made by QGIS.
* \since QGIS 3.10
*/
QString symbolLabel() const;
/**
* Evaluates and returns the text label of the current node
* \param context extra QgsExpressionContext to use for evaluating the expression
* \param label text to evaluate instead of the layer layertree string
* \since QGIS 3.10
*/
QString evaluateLabel( const QgsExpressionContext &context = QgsExpressionContext(), const QString &label = QString() );
public slots:
/**
@ -361,6 +385,12 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
// ident the symbol icon to make it look like a tree structure
static const int INDENT_SIZE = 20;
/**
* Create an expressionContextScope containing symbol related variables
* \since QGIS 3.10
*/
QgsExpressionContextScope *createSymbolScope() const SIP_FACTORY;
/**
* Sets all items belonging to the same layer as this node to the same check state.
* \param state check state

View File

@ -34,10 +34,11 @@
#include <QDomDocument>
#include <QDomElement>
#include <QPainter>
#include "qgsexpressioncontext.h"
QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout )
: QgsLayoutItem( layout )
, mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot() ) )
, mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot(), this ) )
{
#if 0 //no longer required?
connect( &layout->atlasComposition(), &QgsAtlasComposition::renderEnded, this, &QgsLayoutItemLegend::onAtlasEnded );
@ -55,6 +56,7 @@ QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout )
invalidateCache();
update();
} );
connect( mLegendModel.get(), &QgsLegendModel::refreshLegend, this, &QgsLayoutItemLegend::refresh );
}
QgsLayoutItemLegend *QgsLayoutItemLegend::create( QgsLayout *layout )
@ -225,6 +227,7 @@ void QgsLayoutItemLegend::setCustomLayerTree( QgsLayerTree *rootGroup )
mCustomLayerTree.reset( rootGroup );
}
void QgsLayoutItemLegend::setAutoUpdateModel( bool autoUpdate )
{
if ( autoUpdate == autoUpdateModel() )
@ -697,6 +700,7 @@ void QgsLayoutItemLegend::setLinkedMap( QgsLayoutItemMap *map )
}
updateFilterByMap();
}
void QgsLayoutItemLegend::invalidateCurrentMap()
@ -760,9 +764,8 @@ void QgsLayoutItemLegend::mapLayerStyleOverridesChanged()
else
{
mLegendModel->setLayerStyleOverrides( mMap->layerStyleOverrides() );
const auto constFindLayers = mLegendModel->rootGroup()->findLayers();
for ( QgsLayerTreeLayer *nodeLayer : constFindLayers )
const QList< QgsLayerTreeLayer * > layers = mLegendModel->rootGroup()->findLayers();
for ( QgsLayerTreeLayer *nodeLayer : layers )
mLegendModel->refreshLayerLegend( nodeLayer );
}
@ -846,11 +849,8 @@ QgsExpressionContext QgsLayoutItemLegend::createExpressionContext() const
// We only want the last scope from the map's expression context, as this contains
// the map specific variables. We don't want the rest of the map's context, because that
// will contain duplicate global, project, layout, etc scopes.
if ( mMap )
{
context.appendScope( mMap->createExpressionContext().popScope() );
}
QgsExpressionContextScope *scope = new QgsExpressionContextScope( tr( "Legend Settings" ) );
@ -862,16 +862,26 @@ QgsExpressionContext QgsLayoutItemLegend::createExpressionContext() const
scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "legend_filter_out_atlas" ), legendFilterOutAtlas(), true ) );
context.appendScope( scope );
return context;
}
// -------------------------------------------------------------------------
#include "qgslayertreemodellegendnode.h"
#include "qgsvectorlayer.h"
#include "qgsmaplayerlegend.h"
QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent )
QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent, QgsLayoutItemLegend *layout )
: QgsLayerTreeModel( rootNode, parent )
, mLayoutLegend( layout )
{
setFlag( QgsLayerTreeModel::AllowLegendChangeState, false );
setFlag( QgsLayerTreeModel::AllowNodeReorder, true );
}
QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QgsLayoutItemLegend *layout )
: QgsLayerTreeModel( rootNode )
, mLayoutLegend( layout )
{
setFlag( QgsLayerTreeModel::AllowLegendChangeState, false );
setFlag( QgsLayerTreeModel::AllowNodeReorder, true );
@ -880,22 +890,62 @@ QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent )
QVariant QgsLegendModel::data( const QModelIndex &index, int role ) const
{
// handle custom layer node labels
if ( QgsLayerTreeNode *node = index2node( index ) )
{
if ( QgsLayerTree::isLayer( node ) && ( role == Qt::DisplayRole || role == Qt::EditRole ) && !node->customProperty( QStringLiteral( "legend/title-label" ) ).isNull() )
{
QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );
QString name = node->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
if ( nodeLayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt() && role == Qt::DisplayRole )
{
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( nodeLayer->layer() );
if ( vlayer && vlayer->featureCount() >= 0 )
name += QStringLiteral( " [%1]" ).arg( vlayer->featureCount() );
}
return name;
}
}
QgsLayerTreeNode *node = index2node( index );
QgsLayerTreeLayer *nodeLayer = QgsLayerTree::isLayer( node ) ? QgsLayerTree::toLayer( node ) : nullptr;
if ( nodeLayer && ( role == Qt::DisplayRole || role == Qt::EditRole ) )
{
QString name;
QgsVectorLayer *vlayer = qobject_cast<QgsVectorLayer *>( nodeLayer->layer() );
//finding the first label that is stored
name = nodeLayer->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
if ( name.isEmpty() )
name = nodeLayer->name();
if ( name.isEmpty() )
name = node->customProperty( QStringLiteral( "legend/title-label" ) ).toString();
if ( name.isEmpty() )
name = node->name();
if ( nodeLayer->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt() )
{
if ( vlayer && vlayer->featureCount() >= 0 )
{
name += QStringLiteral( " [%1]" ).arg( vlayer->featureCount() );
return name;
}
}
bool evaluate = vlayer ? !nodeLayer->labelExpression().isEmpty() : false;
if ( evaluate || name.contains( "[%" ) )
{
QgsExpressionContext expressionContext;
if ( vlayer )
{
connect( vlayer, &QgsVectorLayer::symbolFeatureCountMapChanged, this, &QgsLegendModel::forceRefresh, Qt::UniqueConnection );
// counting is done here to ensure that a valid vector layer needs to be evaluated, count is used to validate previous count or update the count if invalidated
vlayer->countSymbolFeatures();
}
if ( mLayoutLegend )
expressionContext = mLayoutLegend->createExpressionContext();
else
expressionContext = QgsExpressionContext();
const QList<QgsLayerTreeModelLegendNode *> legendnodes = layerLegendNodes( nodeLayer, false );
if ( legendnodes.count() > 1 ) // evaluate all existing legend nodes but leave the name for the legend evaluator
{
for ( QgsLayerTreeModelLegendNode *treenode : legendnodes )
{
if ( QgsSymbolLegendNode *symnode = qobject_cast<QgsSymbolLegendNode *>( treenode ) )
symnode->evaluateLabel( expressionContext );
}
}
else if ( QgsSymbolLegendNode *symnode = qobject_cast<QgsSymbolLegendNode *>( legendnodes.first() ) )
name = symnode->evaluateLabel( expressionContext, name );
}
return name;
}
return QgsLayerTreeModel::data( index, role );
}
@ -907,3 +957,22 @@ Qt::ItemFlags QgsLegendModel::flags( const QModelIndex &index ) const
return QgsLayerTreeModel::flags( index );
}
QList<QgsLayerTreeModelLegendNode *> QgsLegendModel::layerLegendNodes( QgsLayerTreeLayer *nodeLayer, bool skipNodeEmbeddedInParent ) const
{
if ( !mLegend.contains( nodeLayer ) )
return QList<QgsLayerTreeModelLegendNode *>();
const LayerLegendData &data = mLegend[nodeLayer];
QList<QgsLayerTreeModelLegendNode *> lst( data.activeNodes );
if ( !skipNodeEmbeddedInParent && data.embeddedNodeInParent )
lst.prepend( data.embeddedNodeInParent );
return lst;
}
void QgsLegendModel::forceRefresh()
{
emit refreshLegend();
}

View File

@ -24,11 +24,13 @@
#include "qgslayertreemodel.h"
#include "qgslegendsettings.h"
#include "qgslayertreegroup.h"
#include "qgsexpressioncontext.h"
class QgsLayerTreeModel;
class QgsSymbol;
class QgsLayoutItemMap;
class QgsLegendRenderer;
class QgsLayoutItemLegend;
/**
* \ingroup core
@ -44,11 +46,48 @@ class CORE_EXPORT QgsLegendModel : public QgsLayerTreeModel
public:
//! Construct the model based on the given layer tree
QgsLegendModel( QgsLayerTree *rootNode, QObject *parent SIP_TRANSFERTHIS = nullptr );
QgsLegendModel( QgsLayerTree *rootNode, QObject *parent SIP_TRANSFERTHIS = nullptr, QgsLayoutItemLegend *layout = nullptr );
//! Alternative constructor.
QgsLegendModel( QgsLayerTree *rootNode, QgsLayoutItemLegend *layout );
QVariant data( const QModelIndex &index, int role ) const override;
Qt::ItemFlags flags( const QModelIndex &index ) const override;
signals:
/**
* Emitted to refresh the legend.
* \since QGIS 3.10
*/
void refreshLegend();
private slots:
/**
* Handle incoming signal to refresh the legend.
* \since QGIS 3.10
*/
void forceRefresh();
private:
/**
* Returns filtered list of active legend nodes attached to a particular layer node
* (by default it returns also legend node embedded in parent layer node (if any) unless skipNodeEmbeddedInParent is true)
* \note Parameter skipNodeEmbeddedInParent added in QGIS 2.18
* \see layerOriginalLegendNodes()
* \since QGIS 3.10
*/
QList<QgsLayerTreeModelLegendNode *> layerLegendNodes( QgsLayerTreeLayer *nodeLayer, bool skipNodeEmbeddedInParent = false ) const;
/**
* Pointer to the QgsLayoutItemLegend class that made the model.
* \since QGIS 3.10
*/
QgsLayoutItemLegend *mLayoutLegend = nullptr;
};
@ -458,7 +497,6 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
QgsExpressionContext createExpressionContext() const override;
public slots:
void refresh() override;
@ -486,6 +524,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
void nodeCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key );
private:
QgsLayoutItemLegend() = delete;

View File

@ -2243,3 +2243,4 @@ QgsRectangle QgsLayoutItemMap::computeAtlasRectangle()
return g.boundingBox();
}
}

View File

@ -32,6 +32,7 @@ class QTextCodec;
#include "qgsrelation.h"
#include "qgsfeaturesink.h"
#include "qgsfeaturesource.h"
#include "qgsfeaturerequest.h"
typedef QList<int> QgsAttributeList SIP_SKIP;
typedef QSet<int> QgsAttributeIds SIP_SKIP;
@ -43,7 +44,6 @@ class QgsFeedback;
class QgsFeatureRenderer;
class QgsAbstractVectorLayerLabeling;
#include "qgsfeaturerequest.h"
/**
* \ingroup core

View File

@ -27,7 +27,7 @@
#include <QFont>
#include <QMutex>
#include "qgis_sip.h"
#include "qgis.h"
#include "qgsmaplayer.h"
#include "qgsfeature.h"
#include "qgsfeaturerequest.h"
@ -41,6 +41,7 @@
#include "qgsfeatureiterator.h"
#include "qgsexpressioncontextgenerator.h"
#include "qgsexpressioncontextscopegenerator.h"
#include "qgsexpressioncontext.h"
class QPainter;
class QImage;

View File

@ -15,6 +15,7 @@
#include "qgsvectorlayerfeaturecounter.h"
#include "qgsvectorlayer.h"
#include "qgsfeatureid.h"
QgsVectorLayerFeatureCounter::QgsVectorLayerFeatureCounter( QgsVectorLayer *layer, const QgsExpressionContext &context )
: QgsTask( tr( "Counting features in %1" ).arg( layer->name() ), QgsTask::CanCancel )
@ -31,12 +32,15 @@ QgsVectorLayerFeatureCounter::QgsVectorLayerFeatureCounter( QgsVectorLayer *laye
bool QgsVectorLayerFeatureCounter::run()
{
mSymbolFeatureCountMap.clear();
mSymbolFeatureIdMap.clear();
QgsLegendSymbolList symbolList = mRenderer->legendSymbolItems();
QgsLegendSymbolList::const_iterator symbolIt = symbolList.constBegin();
for ( ; symbolIt != symbolList.constEnd(); ++symbolIt )
{
mSymbolFeatureCountMap.insert( symbolIt->label(), 0 );
mSymbolFeatureIdMap.insert( symbolIt->label(), QgsFeatureIds() );
}
// If there are no features to be counted, we can spare us the trouble
@ -65,11 +69,12 @@ bool QgsVectorLayerFeatureCounter::run()
while ( fit.nextFeature( f ) )
{
renderContext.expressionContext().setFeature( f );
QSet<QString> featureKeyList = mRenderer->legendKeysForFeature( f, renderContext );
const auto constFeatureKeyList = featureKeyList;
for ( const QString &key : constFeatureKeyList )
const QSet<QString> featureKeyList = mRenderer->legendKeysForFeature( f, renderContext );
for ( const QString &key : featureKeyList )
{
mSymbolFeatureCountMap[key] += 1;
mSymbolFeatureIdMap[key].insert( f.id() );
}
++featuresCounted;
@ -88,9 +93,7 @@ bool QgsVectorLayerFeatureCounter::run()
}
mRenderer->stopRender( renderContext );
}
setProgress( 100 );
emit symbolsCounted();
return true;
}
@ -104,3 +107,13 @@ long QgsVectorLayerFeatureCounter::featureCount( const QString &legendKey ) cons
{
return mSymbolFeatureCountMap.value( legendKey, -1 );
}
QHash<QString, QgsFeatureIds> QgsVectorLayerFeatureCounter::symbolFeatureIdMap() const
{
return mSymbolFeatureIdMap;
}
QgsFeatureIds QgsVectorLayerFeatureCounter::featureIds( const QString &symbolkey ) const
{
return mSymbolFeatureIdMap.value( symbolkey, QgsFeatureIds() );
}

View File

@ -18,6 +18,7 @@
#include "qgsvectorlayerfeatureiterator.h"
#include "qgsrenderer.h"
#include "qgstaskmanager.h"
#include "qgsfeatureid.h"
/**
* \ingroup core
@ -40,10 +41,14 @@ class CORE_EXPORT QgsVectorLayerFeatureCounter : public QgsTask
*/
QgsVectorLayerFeatureCounter( QgsVectorLayer *layer, const QgsExpressionContext &context = QgsExpressionContext() );
/**
* Calculates the feature count and Ids per symbol
*/
bool run() override;
/**
* Gets the count for each symbol. Only valid after the symbolsCounted()
* Returns the count for each symbol. Only valid after the symbolsCounted()
* signal has been emitted.
*
* \note Not available in Python bindings.
@ -51,11 +56,29 @@ class CORE_EXPORT QgsVectorLayerFeatureCounter : public QgsTask
QHash<QString, long> symbolFeatureCountMap() const SIP_SKIP;
/**
* Gets the feature count for a particular \a legendKey.
* Returns the feature count for a particular \a legendKey.
* If the key has not been found, -1 will be returned.
*/
long featureCount( const QString &legendKey ) const;
/**
* Returns the QgsFeatureIds for each symbol. Only valid after the symbolsCounted()
* signal has been emitted.
*
* \see symbolFeatureCountMap
* \note Not available in Python bindings.
* \since QGIS 3.10
*/
QHash<QString, QgsFeatureIds> symbolFeatureIdMap() const SIP_SKIP;
/**
* Returns the feature Ids for a particular \a legendKey.
* If the key has not been found an empty QSet will be returned.
*
* \since QGIS 3.10
*/
QgsFeatureIds featureIds( const QString &symbolkey ) const;
signals:
/**
@ -68,6 +91,7 @@ class CORE_EXPORT QgsVectorLayerFeatureCounter : public QgsTask
std::unique_ptr<QgsFeatureRenderer> mRenderer;
QgsExpressionContext mExpressionContext;
QHash<QString, long> mSymbolFeatureCountMap;
QHash<QString, QgsFeatureIds> mSymbolFeatureIdMap;
int mFeatureCount;
};

View File

@ -401,6 +401,23 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mLayerExpressionButton">
<property name="toolTip">
<string> Add an expression to the vector layer and each child symbol's label</string>
</property>
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/mActionAddExpression.svg</normaloff>:/images/themes/default/mActionAddExpression.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">

View File

@ -32,14 +32,15 @@ from qgis.core import (QgsPrintLayout,
QgsExpression,
QgsMapLayerLegendUtils,
QgsLegendStyle,
QgsFontUtils)
QgsFontUtils,
QgsApplication)
from qgis.testing import (start_app,
unittest
)
from utilities import unitTestDataPath
from qgslayoutchecker import QgsLayoutChecker
import os
from time import sleep
from test_qgslayoutitem import LayoutItemTestCase
start_app()
@ -66,6 +67,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().clear()
QgsProject.instance().addMapLayers([point_layer])
marker_symbol = QgsMarkerSymbol.createSimple({'color': '#ff0000', 'outline_style': 'no', 'size': '5', 'size_unit': 'MapUnit'})
@ -123,7 +125,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeWithMapContent(self):
"""Test test legend resizes to match map content"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer])
@ -164,7 +165,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeDisabled(self):
"""Test that test legend does not resize if auto size is disabled"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer])
@ -209,7 +209,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeDisabledCrop(self):
"""Test that if legend resizing is disabled, and legend is too small, then content is cropped"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer])
@ -322,10 +321,8 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testExpressionInText(self):
"""Test expressions embedded in legend node text"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
layout = QgsPrintLayout(QgsProject.instance())
layout.setName('LAYOUT')
layout.initializeDefaults()
@ -378,6 +375,121 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
QgsProject.instance().removeMapLayers([point_layer.id()])
def testSymbolExpressions(self):
"Test expressions embedded in legend node text"
QgsProject.instance().clear()
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
layout = QgsPrintLayout(QgsProject.instance())
layout.initializeDefaults()
map = QgsLayoutItemMap(layout)
map.setLayers([point_layer])
layout.addLayoutItem(map)
map.setExtent(point_layer.extent())
legend = QgsLayoutItemLegend(layout)
layer = QgsProject.instance().addMapLayer(point_layer)
legendlayer = legend.model().rootGroup().addLayer(point_layer)
counterTask = point_layer.countSymbolFeatures()
counterTask.waitForFinished()
TM = QgsApplication.taskManager()
actask = TM.activeTasks()
print(TM.tasks(), actask)
count = actask[0]
legend.model().refreshLayerLegend(legendlayer)
legendnodes = legend.model().layerLegendNodes(legendlayer)
legendnodes[0].setUserLabel('[% @symbol_id %]')
legendnodes[1].setUserLabel('[% @symbol_count %]')
legendnodes[2].setUserLabel('[% sum("Pilots") %]')
label1 = legendnodes[0].evaluateLabel()
label2 = legendnodes[1].evaluateLabel()
label3 = legendnodes[2].evaluateLabel()
count.waitForFinished()
self.assertEqual(label1, '0')
#self.assertEqual(label2, '5')
#self.assertEqual(label3, '12')
legendlayer.setLabelExpression("Concat(@symbol_label, @symbol_id)")
label1 = legendnodes[0].evaluateLabel()
label2 = legendnodes[1].evaluateLabel()
label3 = legendnodes[2].evaluateLabel()
self.assertEqual(label1, ' @symbol_id 0')
#self.assertEqual(label2, '@symbol_count 1')
#self.assertEqual(label3, 'sum("Pilots") 2')
QgsProject.instance().clear()
def testSymbolExpressionRender(self):
"""Test expressions embedded in legend node text"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
layout = QgsPrintLayout(QgsProject.instance())
layout.setName('LAYOUT')
layout.initializeDefaults()
map = QgsLayoutItemMap(layout)
map.attemptSetSceneRect(QRectF(20, 20, 80, 80))
map.setFrameEnabled(True)
map.setLayers([point_layer])
layout.addLayoutItem(map)
map.setExtent(point_layer.extent())
legend = QgsLayoutItemLegend(layout)
legend.setTitle("Legend")
legend.attemptSetSceneRect(QRectF(120, 20, 100, 100))
legend.setFrameEnabled(True)
legend.setFrameStrokeWidth(QgsLayoutMeasurement(2))
legend.setBackgroundColor(QColor(200, 200, 200))
legend.setTitle('')
legend.setLegendFilterByMapEnabled(False)
legend.setStyleFont(QgsLegendStyle.Title, QgsFontUtils.getStandardTestFont('Bold', 16))
legend.setStyleFont(QgsLegendStyle.Group, QgsFontUtils.getStandardTestFont('Bold', 16))
legend.setStyleFont(QgsLegendStyle.Subgroup, QgsFontUtils.getStandardTestFont('Bold', 16))
legend.setStyleFont(QgsLegendStyle.Symbol, QgsFontUtils.getStandardTestFont('Bold', 16))
legend.setStyleFont(QgsLegendStyle.SymbolLabel, QgsFontUtils.getStandardTestFont('Bold', 16))
legend.setAutoUpdateModel(False)
QgsProject.instance().addMapLayers([point_layer])
s = QgsMapSettings()
s.setLayers([point_layer])
group = legend.model().rootGroup().addGroup("Group [% 1 + 5 %] [% @layout_name %]")
layer_tree_layer = group.addLayer(point_layer)
counterTask = point_layer.countSymbolFeatures()
counterTask.waitForFinished() # does this even work?
layer_tree_layer.setCustomProperty("legend/title-label", 'bbbb [% 1+2 %] xx [% @layout_name %] [% @layer_name %]')
QgsMapLayerLegendUtils.setLegendNodeUserLabel(layer_tree_layer, 0, 'xxxx')
legend.model().refreshLayerLegend(layer_tree_layer)
layer_tree_layer.setLabelExpression('Concat(@symbol_id, @symbol_label, count("Class"))')
legend.model().layerLegendNodes(layer_tree_layer)[0].setUserLabel(' sym 1')
legend.model().layerLegendNodes(layer_tree_layer)[1].setUserLabel('[%@symbol_count %]')
legend.model().layerLegendNodes(layer_tree_layer)[2].setUserLabel('[% count("Class") %]')
layout.addLayoutItem(legend)
legend.setLinkedMap(map)
legend.updateLegend()
print(layer_tree_layer.labelExpression())
TM = QgsApplication.taskManager()
actask = TM.activeTasks()
print(TM.tasks(), actask)
count = actask[0]
count.waitForFinished()
map.setExtent(QgsRectangle(-102.51, 41.16, -102.36, 41.30))
checker = QgsLayoutChecker(
'composer_legend_symbol_expression', layout)
checker.setControlPathPrefix("composer_legend")
sleep(4)
result, message = checker.testLayout()
self.assertTrue(result, message)
QgsProject.instance().removeMapLayers([point_layer.id()])
if __name__ == '__main__':
unittest.main()