1
0
mirror of https://github.com/qgis/QGIS.git synced 2025-04-30 00:04:26 -04:00

Symbol aware legend expression ()

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

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

(image error) Size: 2.0 KiB

@ -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) Resolves reference to layer from stored layer ID (if it has not been resolved already)
.. versionadded:: 3.0 .. 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 %End
signals: signals:

@ -12,6 +12,7 @@
class QgsLayerTreeModelLegendNode : QObject class QgsLayerTreeModelLegendNode : QObject
{ {
%Docstring %Docstring
@ -26,6 +27,12 @@ and customized look.
%TypeHeaderCode %TypeHeaderCode
#include "qgslayertreemodellegendnode.h" #include "qgslayertreemodellegendnode.h"
%End
%ConvertToSubClassCode
if ( qobject_cast<QgsSymbolLegendNode *> ( sipCpp ) )
sipType = sipType_QgsSymbolLegendNode;
else
sipType = 0;
%End %End
public: 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. Sets format of text to be shown on top of the symbol.
.. versionadded:: 3.2 .. 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 %End
public slots: public slots:

@ -24,9 +24,14 @@ Overrides some functionality of QgsLayerTreeModel to better fit the needs of lay
#include "qgslayoutitemlegend.h" #include "qgslayoutitemlegend.h"
%End %End
public: public:
QgsLegendModel( QgsLayerTree *rootNode, QObject *parent /TransferThis/ = 0 ); QgsLegendModel( QgsLayerTree *rootNode, QObject *parent /TransferThis/ = 0, QgsLayoutItemLegend *layout = 0 );
%Docstring %Docstring
Construct the model based on the given layer tree Construct the model based on the given layer tree
%End
QgsLegendModel( QgsLayerTree *rootNode, QgsLayoutItemLegend *layout );
%Docstring
Alternative constructor.
%End %End
virtual QVariant data( const QModelIndex &index, int role ) const; 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; 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; virtual QgsExpressionContext createExpressionContext() const;
public slots: public slots:
virtual void refresh(); virtual void refresh();

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

@ -29,16 +29,29 @@ QgsVectorLayer.countSymbolFeatures() and connect to the signal
Create a new feature counter for ``layer``. Create a new feature counter for ``layer``.
%End %End
virtual bool run(); virtual bool run();
%Docstring
Calculates the feature count and Ids per symbol
%End
long featureCount( const QString &legendKey ) const; long featureCount( const QString &legendKey ) const;
%Docstring %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. If the key has not been found, -1 will be returned.
%End %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: signals:
void symbolsCounted(); void symbolsCounted();

@ -38,6 +38,7 @@
#include "qgslayoutitemlegend.h" #include "qgslayoutitemlegend.h"
#include "qgslayoutmeasurementconverter.h" #include "qgslayoutmeasurementconverter.h"
#include "qgsunittypes.h" #include "qgsunittypes.h"
#include "qgsexpressionbuilderdialog.h"
#include <QMessageBox> #include <QMessageBox>
#include <QInputDialog> #include <QInputDialog>
@ -104,6 +105,7 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend )
connect( mEditPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mEditPushButton_clicked ); connect( mEditPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mEditPushButton_clicked );
connect( mCountToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mCountToolButton_clicked ); connect( mCountToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mCountToolButton_clicked );
connect( mExpressionFilterButton, &QgsLegendFilterButton::toggled, this, &QgsLayoutLegendWidget::mExpressionFilterButton_toggled ); 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( mFilterByMapToolButton, &QToolButton::toggled, this, &QgsLayoutLegendWidget::mFilterByMapToolButton_toggled );
connect( mUpdateAllPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mUpdateAllPushButton_clicked ); connect( mUpdateAllPushButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mUpdateAllPushButton_clicked );
connect( mAddGroupToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mAddGroupToolButton_clicked ); connect( mAddGroupToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mAddGroupToolButton_clicked );
@ -136,6 +138,7 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend )
mMoveUpToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowUp.svg" ) ) ); mMoveUpToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowUp.svg" ) ) );
mMoveDownToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowDown.svg" ) ) ); mMoveDownToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionArrowDown.svg" ) ) );
mCountToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionSum.svg" ) ) ); mCountToolButton->setIcon( QIcon( QgsApplication::iconPath( "mActionSum.svg" ) ) );
mLayerExpressionButton->setIcon( QIcon( QgsApplication::iconPath( "mActionAddExpression.svg" ) ) );
mFontColorButton->setColorDialogTitle( tr( "Select Font Color" ) ); mFontColorButton->setColorDialogTitle( tr( "Select Font Color" ) );
mFontColorButton->setContext( QStringLiteral( "composer" ) ); mFontColorButton->setContext( QStringLiteral( "composer" ) );
@ -994,6 +997,45 @@ void QgsLayoutLegendWidget::mExpressionFilterButton_toggled( bool checked )
mLegend->endCommand(); 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() void QgsLayoutLegendWidget::mUpdateAllPushButton_clicked()
{ {
updateLegend(); updateLegend();
@ -1105,12 +1147,27 @@ void QgsLayoutLegendWidget::selectedChanged( const QModelIndex &current, const Q
Q_UNUSED( current ) Q_UNUSED( current )
Q_UNUSED( previous ) Q_UNUSED( previous )
mLayerExpressionButton->setEnabled( false );
if ( mLegend && mLegend->autoUpdateModel() ) 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; return;
}
mCountToolButton->setChecked( false ); mCountToolButton->setChecked( false );
mCountToolButton->setEnabled( false ); mCountToolButton->setEnabled( false );
mExpressionFilterButton->blockSignals( true ); mExpressionFilterButton->blockSignals( true );
mExpressionFilterButton->setChecked( false ); mExpressionFilterButton->setChecked( false );
mExpressionFilterButton->setEnabled( 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->setChecked( currentNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toInt() );
mCountToolButton->setEnabled( true ); mCountToolButton->setEnabled( true );
mLayerExpressionButton->setEnabled( true );
bool exprEnabled; bool exprEnabled;
QString expr = QgsLayerTreeUtils::legendFilterByExpression( *qobject_cast<QgsLayerTreeLayer *>( currentNode ), &exprEnabled ); QString expr = QgsLayerTreeUtils::legendFilterByExpression( *qobject_cast<QgsLayerTreeLayer *>( currentNode ), &exprEnabled );

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

@ -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_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_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 //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_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." ) ); sVariableHelpTexts.insert( QStringLiteral( "cluster_size" ), QCoreApplication::translate( "cluster_size", "Number of symbols contained within a cluster." ) );

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

@ -128,6 +128,20 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode
*/ */
void resolveReferences( const QgsProject *project, bool looseMatching = false ) override; 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: signals:
/** /**
@ -148,6 +162,8 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode
QgsMapLayerRef mRef; QgsMapLayerRef mRef;
//! Layer name - only used if layer does not exist or if mUseLayerName is false //! Layer name - only used if layer does not exist or if mUseLayerName is false
QString mLayerName; QString mLayerName;
//! Expression to evaluate in the legend
QString mLabelExpression;
//! //!
bool mUseLayerName = true; bool mUseLayerName = true;

@ -29,6 +29,11 @@
#include "qgsvectorlayer.h" #include "qgsvectorlayer.h"
#include "qgsrasterrenderer.h" #include "qgsrasterrenderer.h"
#include "qgsexpressioncontextutils.h" #include "qgsexpressioncontextutils.h"
#include "qgsfeatureid.h"
#include "qgslayoutitem.h"
#include "qgsvectorlayerfeaturecounter.h"
#include "qgsexpression.h"
QgsLayerTreeModelLegendNode::QgsLayerTreeModelLegendNode( QgsLayerTreeLayer *nodeL, QObject *parent ) QgsLayerTreeModelLegendNode::QgsLayerTreeModelLegendNode( QgsLayerTreeLayer *nodeL, QObject *parent )
: QObject( parent ) : QObject( parent )
@ -284,6 +289,20 @@ const QgsSymbol *QgsSymbolLegendNode::symbol() const
return mItem.symbol(); 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 ) void QgsSymbolLegendNode::setSymbol( QgsSymbol *symbol )
{ {
if ( !symbol ) if ( !symbol )
@ -648,31 +667,68 @@ void QgsSymbolLegendNode::updateLabel()
bool showFeatureCount = mLayerNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toBool(); bool showFeatureCount = mLayerNode->customProperty( QStringLiteral( "showFeatureCount" ), 0 ).toBool();
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( mLayerNode->layer() ); QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( mLayerNode->layer() );
mLabel = symbolLabel();
if ( mEmbeddedInParent ) if ( showFeatureCount && vl )
{ {
QString layerName = mLayerNode->name(); qlonglong count = mEmbeddedInParent ? vl->featureCount() : vl->featureCount( mItem.ruleKey() ) ;
if ( !mLayerNode->customProperty( QStringLiteral( "legend/title-label" ) ).isNull() ) mLabel += QStringLiteral( " [%1]" ).arg( count != -1 ? QLocale().toString( count ) : tr( "N/A" ) );
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" ) );
}
} }
emit dataChanged(); 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;
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

@ -27,6 +27,7 @@
#include "qgis_sip.h" #include "qgis_sip.h"
#include "qgsrasterdataprovider.h" // for QgsImageFetcher dtor visibility #include "qgsrasterdataprovider.h" // for QgsImageFetcher dtor visibility
#include "qgsexpressioncontext.h"
class QgsLayerTreeLayer; class QgsLayerTreeLayer;
class QgsLayerTreeModel; class QgsLayerTreeModel;
@ -48,6 +49,14 @@ class QgsRenderContext;
class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject
{ {
Q_OBJECT 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: public:
enum LegendNodeRoles enum LegendNodeRoles
@ -228,6 +237,7 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
{ {
Q_OBJECT Q_OBJECT
public: public:
/** /**
@ -319,6 +329,20 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
*/ */
void setTextOnSymbolTextFormat( const QgsTextFormat &format ) { mTextOnSymbolTextFormat = format; } 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: public slots:
/** /**
@ -361,6 +385,12 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode
// ident the symbol icon to make it look like a tree structure // ident the symbol icon to make it look like a tree structure
static const int INDENT_SIZE = 20; 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. * Sets all items belonging to the same layer as this node to the same check state.
* \param state check state * \param state check state

@ -34,10 +34,11 @@
#include <QDomDocument> #include <QDomDocument>
#include <QDomElement> #include <QDomElement>
#include <QPainter> #include <QPainter>
#include "qgsexpressioncontext.h"
QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout ) QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout )
: QgsLayoutItem( layout ) : QgsLayoutItem( layout )
, mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot() ) ) , mLegendModel( new QgsLegendModel( layout->project()->layerTreeRoot(), this ) )
{ {
#if 0 //no longer required? #if 0 //no longer required?
connect( &layout->atlasComposition(), &QgsAtlasComposition::renderEnded, this, &QgsLayoutItemLegend::onAtlasEnded ); connect( &layout->atlasComposition(), &QgsAtlasComposition::renderEnded, this, &QgsLayoutItemLegend::onAtlasEnded );
@ -55,6 +56,7 @@ QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout )
invalidateCache(); invalidateCache();
update(); update();
} ); } );
connect( mLegendModel.get(), &QgsLegendModel::refreshLegend, this, &QgsLayoutItemLegend::refresh );
} }
QgsLayoutItemLegend *QgsLayoutItemLegend::create( QgsLayout *layout ) QgsLayoutItemLegend *QgsLayoutItemLegend::create( QgsLayout *layout )
@ -225,6 +227,7 @@ void QgsLayoutItemLegend::setCustomLayerTree( QgsLayerTree *rootGroup )
mCustomLayerTree.reset( rootGroup ); mCustomLayerTree.reset( rootGroup );
} }
void QgsLayoutItemLegend::setAutoUpdateModel( bool autoUpdate ) void QgsLayoutItemLegend::setAutoUpdateModel( bool autoUpdate )
{ {
if ( autoUpdate == autoUpdateModel() ) if ( autoUpdate == autoUpdateModel() )
@ -697,6 +700,7 @@ void QgsLayoutItemLegend::setLinkedMap( QgsLayoutItemMap *map )
} }
updateFilterByMap(); updateFilterByMap();
} }
void QgsLayoutItemLegend::invalidateCurrentMap() void QgsLayoutItemLegend::invalidateCurrentMap()
@ -760,9 +764,8 @@ void QgsLayoutItemLegend::mapLayerStyleOverridesChanged()
else else
{ {
mLegendModel->setLayerStyleOverrides( mMap->layerStyleOverrides() ); mLegendModel->setLayerStyleOverrides( mMap->layerStyleOverrides() );
const QList< QgsLayerTreeLayer * > layers = mLegendModel->rootGroup()->findLayers();
const auto constFindLayers = mLegendModel->rootGroup()->findLayers(); for ( QgsLayerTreeLayer *nodeLayer : layers )
for ( QgsLayerTreeLayer *nodeLayer : constFindLayers )
mLegendModel->refreshLayerLegend( nodeLayer ); 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 // 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 // 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. // will contain duplicate global, project, layout, etc scopes.
if ( mMap ) if ( mMap )
{
context.appendScope( mMap->createExpressionContext().popScope() ); context.appendScope( mMap->createExpressionContext().popScope() );
}
QgsExpressionContextScope *scope = new QgsExpressionContextScope( tr( "Legend Settings" ) ); 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 ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "legend_filter_out_atlas" ), legendFilterOutAtlas(), true ) );
context.appendScope( scope ); context.appendScope( scope );
return context; return context;
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
#include "qgslayertreemodellegendnode.h" #include "qgslayertreemodellegendnode.h"
#include "qgsvectorlayer.h" #include "qgsvectorlayer.h"
#include "qgsmaplayerlegend.h"
QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent ) QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent, QgsLayoutItemLegend *layout )
: QgsLayerTreeModel( rootNode, parent ) : 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::AllowLegendChangeState, false );
setFlag( QgsLayerTreeModel::AllowNodeReorder, true ); setFlag( QgsLayerTreeModel::AllowNodeReorder, true );
@ -880,22 +890,62 @@ QgsLegendModel::QgsLegendModel( QgsLayerTree *rootNode, QObject *parent )
QVariant QgsLegendModel::data( const QModelIndex &index, int role ) const QVariant QgsLegendModel::data( const QModelIndex &index, int role ) const
{ {
// handle custom layer node labels // 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 ); return QgsLayerTreeModel::data( index, role );
} }
@ -907,3 +957,22 @@ Qt::ItemFlags QgsLegendModel::flags( const QModelIndex &index ) const
return QgsLayerTreeModel::flags( index ); 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();
}

@ -24,11 +24,13 @@
#include "qgslayertreemodel.h" #include "qgslayertreemodel.h"
#include "qgslegendsettings.h" #include "qgslegendsettings.h"
#include "qgslayertreegroup.h" #include "qgslayertreegroup.h"
#include "qgsexpressioncontext.h"
class QgsLayerTreeModel; class QgsLayerTreeModel;
class QgsSymbol; class QgsSymbol;
class QgsLayoutItemMap; class QgsLayoutItemMap;
class QgsLegendRenderer; class QgsLegendRenderer;
class QgsLayoutItemLegend;
/** /**
* \ingroup core * \ingroup core
@ -44,11 +46,48 @@ class CORE_EXPORT QgsLegendModel : public QgsLayerTreeModel
public: public:
//! Construct the model based on the given layer tree //! 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; QVariant data( const QModelIndex &index, int role ) const override;
Qt::ItemFlags flags( const QModelIndex &index ) 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; QgsExpressionContext createExpressionContext() const override;
public slots: public slots:
void refresh() override; void refresh() override;
@ -486,6 +524,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
void nodeCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key ); void nodeCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key );
private: private:
QgsLayoutItemLegend() = delete; QgsLayoutItemLegend() = delete;

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

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

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

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

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

@ -401,6 +401,23 @@
</property> </property>
</widget> </widget>
</item> </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> <item>
<spacer name="horizontalSpacer_4"> <spacer name="horizontalSpacer_4">
<property name="orientation"> <property name="orientation">

@ -32,14 +32,15 @@ from qgis.core import (QgsPrintLayout,
QgsExpression, QgsExpression,
QgsMapLayerLegendUtils, QgsMapLayerLegendUtils,
QgsLegendStyle, QgsLegendStyle,
QgsFontUtils) QgsFontUtils,
QgsApplication)
from qgis.testing import (start_app, from qgis.testing import (start_app,
unittest unittest
) )
from utilities import unitTestDataPath from utilities import unitTestDataPath
from qgslayoutchecker import QgsLayoutChecker from qgslayoutchecker import QgsLayoutChecker
import os import os
from time import sleep
from test_qgslayoutitem import LayoutItemTestCase from test_qgslayoutitem import LayoutItemTestCase
start_app() start_app()
@ -66,6 +67,7 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
point_path = os.path.join(TEST_DATA_DIR, 'points.shp') point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr') point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().clear()
QgsProject.instance().addMapLayers([point_layer]) QgsProject.instance().addMapLayers([point_layer])
marker_symbol = QgsMarkerSymbol.createSimple({'color': '#ff0000', 'outline_style': 'no', 'size': '5', 'size_unit': 'MapUnit'}) 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): def testResizeWithMapContent(self):
"""Test test legend resizes to match map content""" """Test test legend resizes to match map content"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp') point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr') point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer]) QgsProject.instance().addMapLayers([point_layer])
@ -164,7 +165,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeDisabled(self): def testResizeDisabled(self):
"""Test that test legend does not resize if auto size is disabled""" """Test that test legend does not resize if auto size is disabled"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp') point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr') point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer]) QgsProject.instance().addMapLayers([point_layer])
@ -209,7 +209,6 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testResizeDisabledCrop(self): def testResizeDisabledCrop(self):
"""Test that if legend resizing is disabled, and legend is too small, then content is cropped""" """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_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr') point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
QgsProject.instance().addMapLayers([point_layer]) QgsProject.instance().addMapLayers([point_layer])
@ -322,10 +321,8 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
def testExpressionInText(self): def testExpressionInText(self):
"""Test expressions embedded in legend node text""" """Test expressions embedded in legend node text"""
point_path = os.path.join(TEST_DATA_DIR, 'points.shp') point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
point_layer = QgsVectorLayer(point_path, 'points', 'ogr') point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
layout = QgsPrintLayout(QgsProject.instance()) layout = QgsPrintLayout(QgsProject.instance())
layout.setName('LAYOUT') layout.setName('LAYOUT')
layout.initializeDefaults() layout.initializeDefaults()
@ -378,6 +375,121 @@ class TestQgsLayoutItemLegend(unittest.TestCase, LayoutItemTestCase):
QgsProject.instance().removeMapLayers([point_layer.id()]) 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__': if __name__ == '__main__':
unittest.main() unittest.main()