From 39e1f68ccc6dd4918614ac0df1e6bc0a92f6a725 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 25 Jan 2016 13:05:36 +1100 Subject: [PATCH] Fix filtering legend content by map when renderer contains duplicate symbols (fix #14131) Now, we don't test for map content using the symbols but instead use the legend key during the hit test --- python/core/qgsmaphittest.sip | 12 ++- .../qgscategorizedsymbolrendererv2.sip | 2 + .../qgsgraduatedsymbolrendererv2.sip | 7 ++ python/core/symbology-ng/qgsrendererv2.sip | 6 ++ .../symbology-ng/qgsrulebasedrendererv2.sip | 7 ++ .../qgssinglesymbolrendererv2.sip | 2 + src/core/layertree/qgslayertreemodel.cpp | 7 +- src/core/qgsmaphittest.cpp | 21 +++++- src/core/qgsmaphittest.h | 15 +++- .../qgscategorizedsymbolrendererv2.cpp | 30 +++++++- .../qgscategorizedsymbolrendererv2.h | 8 ++ .../qgsgraduatedsymbolrendererv2.cpp | 44 ++++++++++- .../qgsgraduatedsymbolrendererv2.h | 11 +++ src/core/symbology-ng/qgsrendererv2.cpp | 7 ++ src/core/symbology-ng/qgsrendererv2.h | 6 ++ .../symbology-ng/qgsrulebasedrendererv2.cpp | 19 +++++ .../symbology-ng/qgsrulebasedrendererv2.h | 7 ++ .../qgssinglesymbolrendererv2.cpp | 7 ++ .../symbology-ng/qgssinglesymbolrendererv2.h | 2 + .../symbology-ng/qgssymbollayerv2utils.cpp | 2 +- tests/src/core/testqgslegendrenderer.cpp | 70 +++++++++++++++++- .../expected_legend_filter_by_map_dupe.png | Bin 0 -> 5408 bytes ...xpected_legend_filter_by_map_dupe_mask.png | Bin 0 -> 2296 bytes 23 files changed, 278 insertions(+), 14 deletions(-) create mode 100644 tests/testdata/control_images/legend/expected_legend_filter_by_map_dupe/expected_legend_filter_by_map_dupe.png create mode 100644 tests/testdata/control_images/legend/expected_legend_filter_by_map_dupe/expected_legend_filter_by_map_dupe_mask.png diff --git a/python/core/qgsmaphittest.sip b/python/core/qgsmaphittest.sip index 2ee8f9e9df0..c18a7087dff 100644 --- a/python/core/qgsmaphittest.sip +++ b/python/core/qgsmaphittest.sip @@ -29,9 +29,18 @@ class QgsMapHitTest * @param symbol symbol to find * @param layer vector layer * @note added in QGIS 2.12 + * @see legendKeyVisible() */ bool symbolVisible( QgsSymbolV2* symbol, QgsVectorLayer* layer ) const; + /** Tests whether a given legend key is visible for a specified layer. + * @param ruleKey legend rule key + * @param layer vector layer + * @note added in QGIS 2.14 + * @see symbolVisible() + */ + bool legendKeyVisible( const QString& ruleKey, QgsVectorLayer* layer ) const; + protected: //! @note not available in Python bindings @@ -43,11 +52,12 @@ class QgsMapHitTest /** Runs test for visible symbols within a layer * @param vl vector layer * @param usedSymbols set for storage of visible symbols + * @param usedSymbolsRuleKey set of storage of visible legend rule keys * @param context render context * @note added in QGIS 2.12 * @note not available in Python bindings */ - //void runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbols, QgsRenderContext& context ); + //void runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbols, SymbolV2Set& usedSymbolsRuleKey, QgsRenderContext& context ); }; diff --git a/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip b/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip index 971e0639213..e057ae0cd6e 100644 --- a/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip +++ b/python/core/symbology-ng/qgscategorizedsymbolrendererv2.sip @@ -121,6 +121,8 @@ class QgsCategorizedSymbolRendererV2 : QgsFeatureRendererV2 //! @note added in 2.10 QgsLegendSymbolListV2 legendSymbolItemsV2() const; + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ); + QgsSymbolV2* sourceSymbol(); void setSourceSymbol( QgsSymbolV2* sym /Transfer/ ); diff --git a/python/core/symbology-ng/qgsgraduatedsymbolrendererv2.sip b/python/core/symbology-ng/qgsgraduatedsymbolrendererv2.sip index e59a5f44292..1e91a8345a7 100644 --- a/python/core/symbology-ng/qgsgraduatedsymbolrendererv2.sip +++ b/python/core/symbology-ng/qgsgraduatedsymbolrendererv2.sip @@ -224,6 +224,8 @@ class QgsGraduatedSymbolRendererV2 : QgsFeatureRendererV2 //! @note added in 2.10 QgsLegendSymbolListV2 legendSymbolItemsV2() const; + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ); + QgsSymbolV2* sourceSymbol(); void setSourceSymbol( QgsSymbolV2* sym /Transfer/ ); @@ -306,6 +308,11 @@ class QgsGraduatedSymbolRendererV2 : QgsFeatureRendererV2 protected: QgsSymbolV2* symbolForValue( double value ); + /** Returns the matching legend key for a value. + */ + QString legendKeyForValue( double value ) const; + + private: QgsGraduatedSymbolRendererV2( const QgsGraduatedSymbolRendererV2 & ); QgsGraduatedSymbolRendererV2 & operator=( const QgsGraduatedSymbolRendererV2 & ); diff --git a/python/core/symbology-ng/qgsrendererv2.sip b/python/core/symbology-ng/qgsrendererv2.sip index e654fac8166..f8009b8ea1f 100644 --- a/python/core/symbology-ng/qgsrendererv2.sip +++ b/python/core/symbology-ng/qgsrendererv2.sip @@ -98,6 +98,12 @@ class QgsFeatureRendererV2 //TODO - QGIS 3.0 change PyName to originalSymbolForFeature when deprecated method is removed virtual QgsSymbolV2* originalSymbolForFeature( QgsFeature& feature, QgsRenderContext& context ) /PyName=originalSymbolForFeature2/; + /** + * Return legend keys matching a specified feature. + * @note added in 2.14 + */ + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ); + virtual void startRender( QgsRenderContext& context, const QgsFields& fields ) = 0; //! @deprecated since 2.4 - not using QgsVectorLayer directly anymore diff --git a/python/core/symbology-ng/qgsrulebasedrendererv2.sip b/python/core/symbology-ng/qgsrulebasedrendererv2.sip index 098d134497c..67565ccddbf 100644 --- a/python/core/symbology-ng/qgsrulebasedrendererv2.sip +++ b/python/core/symbology-ng/qgsrulebasedrendererv2.sip @@ -220,6 +220,11 @@ class QgsRuleBasedRendererV2 : QgsFeatureRendererV2 //! tell which symbols will be used to render the feature QgsSymbolV2List symbolsForFeature( QgsFeature& feat, QgsRenderContext* context = 0 ); + /** Returns which legend keys match the feature + * @note added in QGIS 2.14 + */ + QSet< QString > legendKeysForFeature( QgsFeature& feat, QgsRenderContext* context = nullptr ); + //! tell which rules will be used to render the feature QList rulesForFeature( QgsFeature& feat, QgsRenderContext* context = 0 ); @@ -388,6 +393,8 @@ class QgsRuleBasedRendererV2 : QgsFeatureRendererV2 virtual QgsSymbolV2List originalSymbolsForFeature( QgsFeature& feat, QgsRenderContext &context ); + virtual QSet legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ); + //! returns bitwise OR-ed capabilities of the renderer virtual int capabilities(); diff --git a/python/core/symbology-ng/qgssinglesymbolrendererv2.sip b/python/core/symbology-ng/qgssinglesymbolrendererv2.sip index 308023f6480..b2ce5942968 100644 --- a/python/core/symbology-ng/qgssinglesymbolrendererv2.sip +++ b/python/core/symbology-ng/qgssinglesymbolrendererv2.sip @@ -63,6 +63,8 @@ class QgsSingleSymbolRendererV2 : QgsFeatureRendererV2 //! @note added in 2.6 virtual QgsLegendSymbolListV2 legendSymbolItemsV2() const; + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ); + //! creates a QgsSingleSymbolRendererV2 from an existing renderer. //! @note added in 2.5 //! @returns a new renderer if the conversion was possible, otherwise 0. diff --git a/src/core/layertree/qgslayertreemodel.cpp b/src/core/layertree/qgslayertreemodel.cpp index 735681c6abd..a7e7d42e592 100644 --- a/src/core/layertree/qgslayertreemodel.cpp +++ b/src/core/layertree/qgslayertreemodel.cpp @@ -1058,12 +1058,13 @@ QList QgsLayerTreeModel::filterLegendNodes( const { Q_FOREACH ( QgsLayerTreeModelLegendNode* node, nodes ) { - QgsSymbolV2* ruleKey = reinterpret_cast< QgsSymbolV2* >( node->data( QgsSymbolV2LegendNode::SymbolV2LegacyRuleKeyRole ).value() ); - if ( ruleKey ) + QgsSymbolV2* symbolKey = reinterpret_cast< QgsSymbolV2* >( node->data( QgsSymbolV2LegendNode::SymbolV2LegacyRuleKeyRole ).value() ); + if ( symbolKey ) { + QString ruleKey = node->data( QgsSymbolV2LegendNode::RuleKeyRole ).toString(); if ( QgsVectorLayer* vl = qobject_cast( node->layerNode()->layer() ) ) { - if ( mLegendFilterHitTest->symbolVisible( ruleKey, vl ) ) + if ( mLegendFilterHitTest->legendKeyVisible( ruleKey, vl ) ) filtered << node; } } diff --git a/src/core/qgsmaphittest.cpp b/src/core/qgsmaphittest.cpp index d4a8980bfa9..99f97c7ab2d 100644 --- a/src/core/qgsmaphittest.cpp +++ b/src/core/qgsmaphittest.cpp @@ -62,6 +62,7 @@ void QgsMapHitTest::run() if ( vl->hasScaleBasedVisibility() && ( mSettings.scale() < vl->minimumScale() || mSettings.scale() > vl->maximumScale() ) ) { mHitTest[vl] = SymbolV2Set(); // no symbols -> will not be shown + mHitTestRuleKey[vl] = SymbolV2Set(); continue; } @@ -74,7 +75,8 @@ void QgsMapHitTest::run() context.expressionContext() << QgsExpressionContextUtils::layerScope( vl ); SymbolV2Set& usedSymbols = mHitTest[vl]; - runHitTestLayer( vl, usedSymbols, context ); + SymbolV2Set& usedSymbolsRuleKey = mHitTestRuleKey[vl]; + runHitTestLayer( vl, usedSymbols, usedSymbolsRuleKey, context ); } painter.end(); @@ -88,7 +90,15 @@ bool QgsMapHitTest::symbolVisible( QgsSymbolV2* symbol, QgsVectorLayer* layer ) return mHitTest.value( layer ).contains( QgsSymbolLayerV2Utils::symbolProperties( symbol ) ); } -void QgsMapHitTest::runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbols, QgsRenderContext& context ) +bool QgsMapHitTest::legendKeyVisible( const QString& ruleKey, QgsVectorLayer* layer ) const +{ + if ( !layer || !mHitTestRuleKey.contains( layer ) ) + return false; + + return mHitTestRuleKey.value( layer ).contains( ruleKey ); +} + +void QgsMapHitTest::runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbols, SymbolV2Set& usedSymbolsRuleKey, QgsRenderContext& context ) { bool hasStyleOverride = mSettings.layerStyleOverrides().contains( vl->id() ); if ( hasStyleOverride ) @@ -125,6 +135,7 @@ void QgsMapHitTest::runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbol QgsFeatureIterator fi = vl->getFeatures( request ); SymbolV2Set lUsedSymbols; + SymbolV2Set lUsedSymbolsRuleKey; bool allExpressionFalse = false; bool hasExpression = mLayerFilterExpression.contains( vl->id() ); QScopedPointer expr; @@ -156,6 +167,11 @@ void QgsMapHitTest::runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbol //make sure we store string representation of symbol, not pointer //otherwise layer style override changes will delete original symbols and leave hanging pointers + Q_FOREACH ( const QString& legendKey, r->legendKeysForFeature( f, context ) ) + { + lUsedSymbolsRuleKey.insert( legendKey ); + } + if ( moreSymbolsPerFeature ) { Q_FOREACH ( QgsSymbolV2* s, r->originalSymbolsForFeature( f, context ) ) @@ -177,6 +193,7 @@ void QgsMapHitTest::runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbol { // QSet is implicitly shared => constant time usedSymbols = lUsedSymbols; + usedSymbolsRuleKey = lUsedSymbolsRuleKey; } if ( hasStyleOverride ) diff --git a/src/core/qgsmaphittest.h b/src/core/qgsmaphittest.h index 21c1f31f2f5..3f982a2efbe 100644 --- a/src/core/qgsmaphittest.h +++ b/src/core/qgsmaphittest.h @@ -52,9 +52,18 @@ class CORE_EXPORT QgsMapHitTest * @param symbol symbol to find * @param layer vector layer * @note added in QGIS 2.12 + * @see legendKeyVisible() */ bool symbolVisible( QgsSymbolV2* symbol, QgsVectorLayer* layer ) const; + /** Tests whether a given legend key is visible for a specified layer. + * @param ruleKey legend rule key + * @param layer vector layer + * @note added in QGIS 2.14 + * @see symbolVisible() + */ + bool legendKeyVisible( const QString& ruleKey, QgsVectorLayer* layer ) const; + protected: //! @note not available in Python bindings @@ -66,11 +75,12 @@ class CORE_EXPORT QgsMapHitTest /** Runs test for visible symbols within a layer * @param vl vector layer * @param usedSymbols set for storage of visible symbols + * @param usedSymbolsRuleKey set of storage of visible legend rule keys * @param context render context * @note added in QGIS 2.12 * @note not available in Python bindings */ - void runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbols, QgsRenderContext& context ); + void runHitTestLayer( QgsVectorLayer* vl, SymbolV2Set& usedSymbols, SymbolV2Set& usedSymbolsRuleKey, QgsRenderContext& context ); //! The initial map settings QgsMapSettings mSettings; @@ -78,6 +88,9 @@ class CORE_EXPORT QgsMapHitTest //! The hit test HitTest mHitTest; + //! The hit test, using legend rule keys + HitTest mHitTestRuleKey; + //! List of expression filter for each layer LayerFilterExpression mLayerFilterExpression; diff --git a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp index 407fa4c9be3..2b86994f4e2 100644 --- a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp +++ b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.cpp @@ -245,9 +245,8 @@ QgsSymbolV2* QgsCategorizedSymbolRendererV2::symbolForFeature( QgsFeature& featu } -QgsSymbolV2* QgsCategorizedSymbolRendererV2::originalSymbolForFeature( QgsFeature& feature, QgsRenderContext &context ) +QVariant QgsCategorizedSymbolRendererV2::valueForFeature( QgsFeature& feature, QgsRenderContext &context ) const { - Q_UNUSED( context ); QgsAttributes attrs = feature.attributes(); QVariant value; if ( mAttrNum == -1 ) @@ -261,6 +260,13 @@ QgsSymbolV2* QgsCategorizedSymbolRendererV2::originalSymbolForFeature( QgsFeatur value = attrs.value( mAttrNum ); } + return value; +} + +QgsSymbolV2* QgsCategorizedSymbolRendererV2::originalSymbolForFeature( QgsFeature& feature, QgsRenderContext &context ) +{ + QVariant value = valueForFeature( feature, context ); + // find the right symbol for the category QgsSymbolV2 *symbol = symbolForValue( value ); if ( symbol == skipRender() ) @@ -859,6 +865,26 @@ QgsLegendSymbolListV2 QgsCategorizedSymbolRendererV2::legendSymbolItemsV2() cons return QgsFeatureRendererV2::legendSymbolItemsV2(); } +QSet QgsCategorizedSymbolRendererV2::legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) +{ + QString value = valueForFeature( feature, context ).toString(); + int i = 0; + + Q_FOREACH ( const QgsRendererCategoryV2& cat, mCategories ) + { + if ( value == cat.value() ) + { + if ( cat.renderState() ) + return QSet< QString >() << QString::number( i ); + else + return QSet< QString >(); + } + i++; + } + + return QSet< QString >(); +} + QgsSymbolV2* QgsCategorizedSymbolRendererV2::sourceSymbol() { return mSourceSymbol.data(); diff --git a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h index 5303bbdaec4..e3f09093bdf 100644 --- a/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h +++ b/src/core/symbology-ng/qgscategorizedsymbolrendererv2.h @@ -150,6 +150,8 @@ class CORE_EXPORT QgsCategorizedSymbolRendererV2 : public QgsFeatureRendererV2 //! @note added in 2.10 QgsLegendSymbolListV2 legendSymbolItemsV2() const override; + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) override; + QgsSymbolV2* sourceSymbol(); void setSourceSymbol( QgsSymbolV2* sym ); @@ -229,6 +231,12 @@ class CORE_EXPORT QgsCategorizedSymbolRendererV2 : public QgsFeatureRendererV2 QgsSymbolV2* skipRender(); QgsSymbolV2* symbolForValue( const QVariant& value ); + + private: + + /** Returns calculated classification value for a feature */ + QVariant valueForFeature( QgsFeature& feature, QgsRenderContext &context ) const; + }; Q_NOWARN_DEPRECATED_POP diff --git a/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.cpp b/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.cpp index 7a68c1192ef..ae299d3ec28 100644 --- a/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.cpp +++ b/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.cpp @@ -325,6 +325,24 @@ QgsSymbolV2* QgsGraduatedSymbolRendererV2::symbolForValue( double value ) return nullptr; } +QString QgsGraduatedSymbolRendererV2::legendKeyForValue( double value ) const +{ + int i = 0; + Q_FOREACH ( const QgsRendererRangeV2& range, mRanges ) + { + if ( range.lowerValue() <= value && range.upperValue() >= value ) + { + if ( range.renderState() || mCounting ) + return QString::number( i ); + else + return QString::null; + } + i++; + } + // the value is out of the range: return NULL + return QString::null; +} + QgsSymbolV2* QgsGraduatedSymbolRendererV2::symbolForFeature( QgsFeature& feature, QgsRenderContext &context ) { QgsSymbolV2* symbol = originalSymbolForFeature( feature, context ); @@ -357,9 +375,8 @@ QgsSymbolV2* QgsGraduatedSymbolRendererV2::symbolForFeature( QgsFeature& feature return tempSymbol; } -QgsSymbolV2* QgsGraduatedSymbolRendererV2::originalSymbolForFeature( QgsFeature& feature, QgsRenderContext &context ) +QVariant QgsGraduatedSymbolRendererV2::valueForFeature( QgsFeature& feature, QgsRenderContext &context ) const { - Q_UNUSED( context ); QgsAttributes attrs = feature.attributes(); QVariant value; if ( mAttrNum < 0 || mAttrNum >= attrs.count() ) @@ -371,6 +388,13 @@ QgsSymbolV2* QgsGraduatedSymbolRendererV2::originalSymbolForFeature( QgsFeature& value = attrs.at( mAttrNum ); } + return value; +} + +QgsSymbolV2* QgsGraduatedSymbolRendererV2::originalSymbolForFeature( QgsFeature& feature, QgsRenderContext &context ) +{ + QVariant value = valueForFeature( feature, context ); + // Null values should not be categorized if ( value.isNull() ) return nullptr; @@ -1245,6 +1269,22 @@ QgsLegendSymbolListV2 QgsGraduatedSymbolRendererV2::legendSymbolItemsV2() const return QgsFeatureRendererV2::legendSymbolItemsV2(); } +QSet< QString > QgsGraduatedSymbolRendererV2::legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) +{ + QVariant value = valueForFeature( feature, context ); + + // Null values should not be categorized + if ( value.isNull() ) + return QSet< QString >(); + + // find the right category + QString key = legendKeyForValue( value.toDouble() ); + if ( !key.isNull() ) + return QSet< QString >() << key; + else + return QSet< QString >(); +} + QgsLegendSymbolList QgsGraduatedSymbolRendererV2::legendSymbolItems( double scaleDenominator, const QString& rule ) { Q_UNUSED( scaleDenominator ); diff --git a/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.h b/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.h index 5bd336affef..7e4dbf90ed7 100644 --- a/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.h +++ b/src/core/symbology-ng/qgsgraduatedsymbolrendererv2.h @@ -257,6 +257,7 @@ class CORE_EXPORT QgsGraduatedSymbolRendererV2 : public QgsFeatureRendererV2 //! @note added in 2.10 QgsLegendSymbolListV2 legendSymbolItemsV2() const override; + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) override; QgsSymbolV2* sourceSymbol(); void setSourceSymbol( QgsSymbolV2* sym ); @@ -360,9 +361,19 @@ class CORE_EXPORT QgsGraduatedSymbolRendererV2 : public QgsFeatureRendererV2 QgsSymbolV2* symbolForValue( double value ); + /** Returns the matching legend key for a value. + */ + QString legendKeyForValue( double value ) const; + //! @note not available in Python bindings static const char * graduatedMethodStr( GraduatedMethod method ); + private: + + /** Returns calculated value used for classifying a feature. + */ + QVariant valueForFeature( QgsFeature& feature, QgsRenderContext &context ) const; + }; Q_NOWARN_DEPRECATED_POP diff --git a/src/core/symbology-ng/qgsrendererv2.cpp b/src/core/symbology-ng/qgsrendererv2.cpp index a9051834382..89855a0ecec 100644 --- a/src/core/symbology-ng/qgsrendererv2.cpp +++ b/src/core/symbology-ng/qgsrendererv2.cpp @@ -271,6 +271,13 @@ QgsSymbolV2 *QgsFeatureRendererV2::originalSymbolForFeature( QgsFeature &feature return symbolForFeature( feature, context ); } +QSet< QString > QgsFeatureRendererV2::legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) +{ + Q_UNUSED( feature ); + Q_UNUSED( context ); + return QSet< QString >(); +} + void QgsFeatureRendererV2::startRender( QgsRenderContext& context, const QgsVectorLayer* vlayer ) { startRender( context, vlayer->fields() ); diff --git a/src/core/symbology-ng/qgsrendererv2.h b/src/core/symbology-ng/qgsrendererv2.h index a92687e693b..9d174824f9a 100644 --- a/src/core/symbology-ng/qgsrendererv2.h +++ b/src/core/symbology-ng/qgsrendererv2.h @@ -119,6 +119,12 @@ class CORE_EXPORT QgsFeatureRendererV2 //TODO - QGIS 3.0 change PyName to originalSymbolForFeature when deprecated method is removed virtual QgsSymbolV2* originalSymbolForFeature( QgsFeature& feature, QgsRenderContext& context ); + /** + * Return legend keys matching a specified feature. + * @note added in 2.14 + */ + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ); + /** * Needs to be called when a new render cycle is started * diff --git a/src/core/symbology-ng/qgsrulebasedrendererv2.cpp b/src/core/symbology-ng/qgsrulebasedrendererv2.cpp index 2260ca045cf..d6e119fc0dd 100644 --- a/src/core/symbology-ng/qgsrulebasedrendererv2.cpp +++ b/src/core/symbology-ng/qgsrulebasedrendererv2.cpp @@ -612,6 +612,20 @@ QgsSymbolV2List QgsRuleBasedRendererV2::Rule::symbolsForFeature( QgsFeature& fea return lst; } +QSet QgsRuleBasedRendererV2::Rule::legendKeysForFeature( QgsFeature& feat, QgsRenderContext* context ) +{ + QSet< QString> lst; + if ( !isFilterOK( feat, context ) ) + return lst; + lst.insert( mRuleKey ); + + Q_FOREACH ( Rule* rule, mActiveChildren ) + { + lst.unite( rule->legendKeysForFeature( feat, context ) ); + } + return lst; +} + QgsRuleBasedRendererV2::RuleList QgsRuleBasedRendererV2::Rule::rulesForFeature( QgsFeature& feat, QgsRenderContext* context ) { RuleList lst; @@ -1193,6 +1207,11 @@ QgsSymbolV2List QgsRuleBasedRendererV2::originalSymbolsForFeature( QgsFeature& f return mRootRule->symbolsForFeature( feat, &context ); } +QSet< QString > QgsRuleBasedRendererV2::legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) +{ + return mRootRule->legendKeysForFeature( feature, &context ); +} + QgsRuleBasedRendererV2* QgsRuleBasedRendererV2::convertFromRenderer( const QgsFeatureRendererV2* renderer ) { if ( renderer->type() == "RuleRenderer" ) diff --git a/src/core/symbology-ng/qgsrulebasedrendererv2.h b/src/core/symbology-ng/qgsrulebasedrendererv2.h index ce76e5c8f30..321d9e1d864 100644 --- a/src/core/symbology-ng/qgsrulebasedrendererv2.h +++ b/src/core/symbology-ng/qgsrulebasedrendererv2.h @@ -263,6 +263,11 @@ class CORE_EXPORT QgsRuleBasedRendererV2 : public QgsFeatureRendererV2 //! tell which symbols will be used to render the feature QgsSymbolV2List symbolsForFeature( QgsFeature& feat, QgsRenderContext* context = nullptr ); + /** Returns which legend keys match the feature + * @note added in QGIS 2.14 + */ + QSet< QString > legendKeysForFeature( QgsFeature& feat, QgsRenderContext* context = nullptr ); + //! tell which rules will be used to render the feature RuleList rulesForFeature( QgsFeature& feat, QgsRenderContext* context = nullptr ); @@ -448,6 +453,8 @@ class CORE_EXPORT QgsRuleBasedRendererV2 : public QgsFeatureRendererV2 virtual QgsSymbolV2List originalSymbolsForFeature( QgsFeature& feat, QgsRenderContext& context ) override; + virtual QSet legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) override; + //! returns bitwise OR-ed capabilities of the renderer virtual int capabilities() override { return MoreSymbolsPerFeature | Filter | ScaleDependent; } diff --git a/src/core/symbology-ng/qgssinglesymbolrendererv2.cpp b/src/core/symbology-ng/qgssinglesymbolrendererv2.cpp index 783a473964b..9b6475cfa67 100644 --- a/src/core/symbology-ng/qgssinglesymbolrendererv2.cpp +++ b/src/core/symbology-ng/qgssinglesymbolrendererv2.cpp @@ -441,6 +441,13 @@ QgsLegendSymbolListV2 QgsSingleSymbolRendererV2::legendSymbolItemsV2() const return lst; } +QSet< QString > QgsSingleSymbolRendererV2::legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) +{ + Q_UNUSED( feature ); + Q_UNUSED( context ); + return QSet< QString >() << QString(); +} + QgsSingleSymbolRendererV2* QgsSingleSymbolRendererV2::convertFromRenderer( const QgsFeatureRendererV2 *renderer ) { if ( renderer->type() == "singleSymbol" ) diff --git a/src/core/symbology-ng/qgssinglesymbolrendererv2.h b/src/core/symbology-ng/qgssinglesymbolrendererv2.h index fea4b20e816..1d3e3156dc6 100644 --- a/src/core/symbology-ng/qgssinglesymbolrendererv2.h +++ b/src/core/symbology-ng/qgssinglesymbolrendererv2.h @@ -84,6 +84,8 @@ class CORE_EXPORT QgsSingleSymbolRendererV2 : public QgsFeatureRendererV2 //! @note added in 2.6 virtual QgsLegendSymbolListV2 legendSymbolItemsV2() const override; + virtual QSet< QString > legendKeysForFeature( QgsFeature& feature, QgsRenderContext& context ) override; + //! creates a QgsSingleSymbolRendererV2 from an existing renderer. //! @note added in 2.5 //! @returns a new renderer if the conversion was possible, otherwise 0. diff --git a/src/core/symbology-ng/qgssymbollayerv2utils.cpp b/src/core/symbology-ng/qgssymbollayerv2utils.cpp index 755bd572c3f..8c1f2f51cef 100644 --- a/src/core/symbology-ng/qgssymbollayerv2utils.cpp +++ b/src/core/symbology-ng/qgssymbollayerv2utils.cpp @@ -1077,7 +1077,7 @@ QDomElement QgsSymbolLayerV2Utils::saveSymbol( const QString& name, QgsSymbolV2* symEl.setAttribute( "name", name ); symEl.setAttribute( "alpha", QString::number( symbol->alpha() ) ); symEl.setAttribute( "clip_to_extent", symbol->clipFeaturesToExtent() ? "1" : "0" ); - QgsDebugMsg( "num layers " + QString::number( symbol->symbolLayerCount() ) ); + //QgsDebugMsg( "num layers " + QString::number( symbol->symbolLayerCount() ) ); for ( int i = 0; i < symbol->symbolLayerCount(); i++ ) { diff --git a/tests/src/core/testqgslegendrenderer.cpp b/tests/src/core/testqgslegendrenderer.cpp index c034e6160d1..737a82e0b1b 100644 --- a/tests/src/core/testqgslegendrenderer.cpp +++ b/tests/src/core/testqgslegendrenderer.cpp @@ -42,7 +42,7 @@ static QString _fileNameForTest( const QString& testName ) return QDir::tempPath() + '/' + testName + ".png"; } -static void _setStandardTestFont( QgsLegendSettings& settings ) +static void _setStandardTestFont( QgsLegendSettings& settings, QString style = "Roman" ) { QList< QgsComposerLegendStyle::Style> styles; styles << QgsComposerLegendStyle::Title @@ -51,7 +51,7 @@ static void _setStandardTestFont( QgsLegendSettings& settings ) << QgsComposerLegendStyle::SymbolLabel; Q_FOREACH ( QgsComposerLegendStyle::Style st, styles ) { - QFont font( QgsFontUtils::getStandardTestFont() ); + QFont font( QgsFontUtils::getStandardTestFont( style ) ); font.setPointSizeF( settings.style( st ).font().pointSizeF() ); settings.rstyle( st ).setFont( font ); } @@ -113,6 +113,7 @@ class TestQgsLegendRenderer : public QObject void testLongSymbolText(); void testThreeColumns(); void testFilterByMap(); + void testFilterByMapSameSymbol(); void testRasterBorder(); void testFilterByPolygon(); void testFilterByExpression(); @@ -345,6 +346,71 @@ void TestQgsLegendRenderer::testFilterByMap() QVERIFY( _verifyImage( testName, mReport ) ); } +void TestQgsLegendRenderer::testFilterByMapSameSymbol() +{ + QgsVectorLayer* vl4 = new QgsVectorLayer( "Point", "Point Layer", "memory" ); + { + QgsVectorDataProvider* pr = vl4->dataProvider(); + QList attrs; + attrs << QgsField( "test_attr", QVariant::Int ); + pr->addAttributes( attrs ); + + QgsFields fields; + fields.append( attrs.back() ); + + QList features; + QgsFeature f1( fields, 1 ); + f1.setAttribute( 0, 1 ); + f1.setGeometry( QgsGeometry::fromPoint( QgsPoint( 1.0, 1.0 ) ) ); + QgsFeature f2( fields, 2 ); + f2.setAttribute( 0, 2 ); + f2.setGeometry( QgsGeometry::fromPoint( QgsPoint( 9.0, 1.0 ) ) ); + QgsFeature f3( fields, 3 ); + f3.setAttribute( 0, 3 ); + f3.setGeometry( QgsGeometry::fromPoint( QgsPoint( 5.0, 5.0 ) ) ); + features << f1 << f2 << f3; + pr->addFeatures( features ); + vl4->updateFields(); + } + QgsMapLayerRegistry::instance()->addMapLayer( vl4 ); + + //setup categorized renderer with duplicate symbols + QgsCategoryList cats; + QgsMarkerSymbolV2* sym4_1 = new QgsMarkerSymbolV2(); + sym4_1->setColor( Qt::red ); + cats << QgsRendererCategoryV2( 1, sym4_1, "Red1" ); + QgsMarkerSymbolV2* sym4_2 = new QgsMarkerSymbolV2(); + sym4_2->setColor( Qt::red ); + cats << QgsRendererCategoryV2( 2, sym4_2, "Red2" ); + QgsMarkerSymbolV2* sym4_3 = new QgsMarkerSymbolV2(); + sym4_3->setColor( Qt::red ); + cats << QgsRendererCategoryV2( 3, sym4_3, "Red3" ); + QgsCategorizedSymbolRendererV2* r4 = new QgsCategorizedSymbolRendererV2( "test_attr", cats ); + vl4->setRendererV2( r4 ); + + QString testName = "legend_filter_by_map_dupe"; + + QgsLayerTreeGroup* root = new QgsLayerTreeGroup(); + root->addLayer( vl4 ); + QgsLayerTreeModel legendModel( root ); + + QgsMapSettings mapSettings; + // extent and size to include only the red and green points + mapSettings.setExtent( QgsRectangle( 0, 0, 10.0, 4.0 ) ); + mapSettings.setOutputSize( QSize( 400, 100 ) ); + mapSettings.setOutputDpi( 96 ); + mapSettings.setLayers( QStringList() << vl4->id() ); + + legendModel.setLegendFilterByMap( &mapSettings ); + + QgsLegendSettings settings; + _setStandardTestFont( settings, "Bold" ); + _renderLegend( testName, &legendModel, settings ); + QVERIFY( _verifyImage( testName, mReport ) ); + + QgsMapLayerRegistry::instance()->removeMapLayer( vl4 ); +} + void TestQgsLegendRenderer::testRasterBorder() { QString testName = "legend_raster_border"; diff --git a/tests/testdata/control_images/legend/expected_legend_filter_by_map_dupe/expected_legend_filter_by_map_dupe.png b/tests/testdata/control_images/legend/expected_legend_filter_by_map_dupe/expected_legend_filter_by_map_dupe.png new file mode 100644 index 0000000000000000000000000000000000000000..9bdfaaab42bcd74e21f9281bdc4fdc213e4da9d3 GIT binary patch literal 5408 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 literal 0 HcmV?d00001