Add method QgsCategorizedSymbolRenderer::matchToSymbols which

matches existing categories to symbol names from a QgsStyle
object and copies matching symbols to these categories
This commit is contained in:
Nyall Dawson 2018-09-07 13:39:17 +10:00
parent 97c95803c6
commit 97a964af4a
5 changed files with 259 additions and 15 deletions

View File

@ -255,6 +255,26 @@ Returns configuration of appearance of legend when using data-defined size for m
Will return null if the functionality is disabled.
.. versionadded:: 3.0
%End
int matchToSymbols( QgsStyle *style, QgsSymbol::SymbolType type,
QVariantList &unmatchedCategories /Out/, QStringList &unmatchedSymbols /Out/, bool caseSensitive = true, bool useTolerantMatch = false );
%Docstring
Replaces category symbols with the symbols from a ``style`` that have a matching
name and symbol ``type``.
The ``unmatchedCategories`` list will be filled with all existing categories which could not be matched
to a symbol in ``style``.
The ``unmatchedSymbols`` list will be filled with all symbol names from ``style`` which were not be matched
to an existing category.
If ``caseSensitive`` is false, then a case-insensitive match will be performed. If ``useTolerantMatch``
is true, then non-alphanumeric characters in style and category names will be ignored during the match.
Returns the count of symbols matched.
.. versionadded:: 3.4
%End
protected:

View File

@ -29,6 +29,7 @@
#include "qgsvectorlayer.h"
#include "qgslogger.h"
#include "qgsproperty.h"
#include "qgsstyle.h"
#include <QDomDocument>
#include <QDomElement>
@ -956,3 +957,65 @@ QgsDataDefinedSizeLegend *QgsCategorizedSymbolRenderer::dataDefinedSizeLegend()
{
return mDataDefinedSizeLegend.get();
}
int QgsCategorizedSymbolRenderer::matchToSymbols( QgsStyle *style, const QgsSymbol::SymbolType type, QVariantList &unmatchedCategories, QStringList &unmatchedSymbols, const bool caseSensitive, const bool useTolerantMatch )
{
if ( !style )
return 0;
int matched = 0;
unmatchedSymbols = style->symbolNames();
const QSet< QString > allSymbolNames = unmatchedSymbols.toSet();
const QRegularExpression tolerantMatchRe( QStringLiteral( "[^\\w\\d ]" ), QRegularExpression::UseUnicodePropertiesOption );
for ( int catIdx = 0; catIdx < mCategories.count(); ++catIdx )
{
const QVariant value = mCategories.at( catIdx ).value();
const QString val = value.toString().trimmed();
std::unique_ptr< QgsSymbol > symbol( style->symbol( val ) );
// case-sensitive match
if ( symbol && symbol->type() == type )
{
matched++;
unmatchedSymbols.removeAll( val );
updateCategorySymbol( catIdx, symbol.release() );
continue;
}
if ( !caseSensitive || useTolerantMatch )
{
QString testVal = val;
if ( useTolerantMatch )
testVal.replace( tolerantMatchRe, QString() );
bool foundMatch = false;
for ( const QString &name : allSymbolNames )
{
QString testName = name.trimmed();
if ( useTolerantMatch )
testName.replace( tolerantMatchRe, QString() );
if ( testName == testVal || ( !caseSensitive && testName.trimmed().compare( testVal, Qt::CaseInsensitive ) == 0 ) )
{
// found a case-insensitive match
std::unique_ptr< QgsSymbol > symbol( style->symbol( name ) );
if ( symbol && symbol->type() == type )
{
matched++;
unmatchedSymbols.removeAll( name );
updateCategorySymbol( catIdx, symbol.release() );
foundMatch = true;
break;
}
}
}
if ( foundMatch )
continue;
}
unmatchedCategories << value;
}
return matched;
}

View File

@ -26,6 +26,7 @@
#include <QHash>
class QgsVectorLayer;
class QgsStyle;
/**
* \ingroup core
@ -225,6 +226,26 @@ class CORE_EXPORT QgsCategorizedSymbolRenderer : public QgsFeatureRenderer
*/
QgsDataDefinedSizeLegend *dataDefinedSizeLegend() const;
/**
* Replaces category symbols with the symbols from a \a style that have a matching
* name and symbol \a type.
*
* The \a unmatchedCategories list will be filled with all existing categories which could not be matched
* to a symbol in \a style.
*
* The \a unmatchedSymbols list will be filled with all symbol names from \a style which were not be matched
* to an existing category.
*
* If \a caseSensitive is false, then a case-insensitive match will be performed. If \a useTolerantMatch
* is true, then non-alphanumeric characters in style and category names will be ignored during the match.
*
* Returns the count of symbols matched.
*
* \since QGIS 3.4
*/
int matchToSymbols( QgsStyle *style, QgsSymbol::SymbolType type,
QVariantList &unmatchedCategories SIP_OUT, QStringList &unmatchedSymbols SIP_OUT, bool caseSensitive = true, bool useTolerantMatch = false );
protected:
QString mAttrName;
QgsCategoryList mCategories;

View File

@ -921,20 +921,14 @@ int QgsCategorizedSymbolRendererWidget::matchToSymbols( QgsStyle *style )
if ( !mLayer || !style )
return 0;
int matched = 0;
for ( int catIdx = 0; catIdx < mRenderer->categories().count(); ++catIdx )
{
QString val = mRenderer->categories().at( catIdx ).value().toString();
std::unique_ptr< QgsSymbol > symbol( style->symbol( val ) );
if ( symbol &&
( ( symbol->type() == QgsSymbol::Marker && mLayer->geometryType() == QgsWkbTypes::PointGeometry )
|| ( symbol->type() == QgsSymbol::Line && mLayer->geometryType() == QgsWkbTypes::LineGeometry )
|| ( symbol->type() == QgsSymbol::Fill && mLayer->geometryType() == QgsWkbTypes::PolygonGeometry ) ) )
{
matched++;
mRenderer->updateCategorySymbol( catIdx, symbol.release() );
}
}
const QgsSymbol::SymbolType type = mLayer->geometryType() == QgsWkbTypes::PointGeometry ? QgsSymbol::Marker
: mLayer->geometryType() == QgsWkbTypes::LineGeometry ? QgsSymbol::Line
: QgsSymbol::Fill;
QVariantList unmatchedCategories;
QStringList unmatchedSymbols;
const int matched = mRenderer->matchToSymbols( style, type, unmatchedCategories, unmatchedSymbols );
mModel->updateSymbology();
return matched;
}

View File

@ -18,10 +18,14 @@ from qgis.testing import unittest, start_app
from qgis.core import (QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsMarkerSymbol,
QgsLineSymbol,
QgsFillSymbol,
QgsField,
QgsFields,
QgsFeature,
QgsRenderContext
QgsRenderContext,
QgsSymbol,
QgsStyle
)
from qgis.PyQt.QtCore import QVariant
from qgis.PyQt.QtGui import QColor
@ -38,6 +42,20 @@ def createMarkerSymbol():
return symbol
def createLineSymbol():
symbol = QgsLineSymbol.createSimple({
"color": "100,150,50"
})
return symbol
def createFillSymbol():
symbol = QgsFillSymbol.createSimple({
"color": "100,150,50"
})
return symbol
class TestQgsCategorizedSymbolRenderer(unittest.TestCase):
def testFilter(self):
@ -312,6 +330,134 @@ class TestQgsCategorizedSymbolRenderer(unittest.TestCase):
renderer.stopRender(context)
def testMatchToSymbols(self):
"""
Test QgsCategorizedSymbolRender.matchToSymbols
"""
renderer = QgsCategorizedSymbolRenderer()
renderer.setClassAttribute('x')
symbol_a = createMarkerSymbol()
symbol_a.setColor(QColor(255, 0, 0))
renderer.addCategory(QgsRendererCategory('a', symbol_a, 'a'))
symbol_b = createMarkerSymbol()
symbol_b.setColor(QColor(0, 255, 0))
renderer.addCategory(QgsRendererCategory('b', symbol_b, 'b'))
symbol_c = createMarkerSymbol()
symbol_c.setColor(QColor(0, 0, 255))
renderer.addCategory(QgsRendererCategory('c ', symbol_c, 'c'))
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(None, QgsSymbol.Marker)
self.assertEqual(matched, 0)
style = QgsStyle()
symbol_a = createMarkerSymbol()
symbol_a.setColor(QColor(255, 10, 10))
self.assertTrue(style.addSymbol('a', symbol_a))
symbol_B = createMarkerSymbol()
symbol_B.setColor(QColor(10, 255, 10))
self.assertTrue(style.addSymbol('B ', symbol_B))
symbol_b = createFillSymbol()
symbol_b.setColor(QColor(10, 255, 10))
self.assertTrue(style.addSymbol('b', symbol_b))
symbol_C = createLineSymbol()
symbol_C.setColor(QColor(10, 255, 10))
self.assertTrue(style.addSymbol('C', symbol_C))
symbol_C = createMarkerSymbol()
symbol_C.setColor(QColor(10, 255, 10))
self.assertTrue(style.addSymbol(' ----c/- ', symbol_C))
# non-matching symbol type
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Line)
self.assertEqual(matched, 0)
self.assertEqual(unmatched_cats, ['a', 'b', 'c '])
self.assertEqual(unmatched_symbols, [' ----c/- ', 'B ', 'C', 'a', 'b'])
# exact match
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker)
self.assertEqual(matched, 1)
self.assertEqual(unmatched_cats, ['b', 'c '])
self.assertEqual(unmatched_symbols, [' ----c/- ', 'B ', 'C', 'b'])
# make sure symbol was applied
context = QgsRenderContext()
renderer.startRender(context, QgsFields())
symbol, ok = renderer.symbolForValue2('a')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#ff0a0a')
renderer.stopRender(context)
# case insensitive match
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, False)
self.assertEqual(matched, 2)
self.assertEqual(unmatched_cats, ['c '])
self.assertEqual(unmatched_symbols, [' ----c/- ', 'C', 'b'])
# make sure symbols were applied
context = QgsRenderContext()
renderer.startRender(context, QgsFields())
symbol, ok = renderer.symbolForValue2('a')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#ff0a0a')
symbol, ok = renderer.symbolForValue2('b')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#0aff0a')
renderer.stopRender(context)
# case insensitive match
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, False)
self.assertEqual(matched, 2)
self.assertEqual(unmatched_cats, ['c '])
self.assertEqual(unmatched_symbols, [' ----c/- ', 'C', 'b'])
# make sure symbols were applied
context = QgsRenderContext()
renderer.startRender(context, QgsFields())
symbol, ok = renderer.symbolForValue2('a')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#ff0a0a')
symbol, ok = renderer.symbolForValue2('b')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#0aff0a')
renderer.stopRender(context)
# tolerant match
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, True, True)
self.assertEqual(matched, 2)
self.assertEqual(unmatched_cats, ['b'])
self.assertEqual(unmatched_symbols, ['B ', 'C', 'b'])
# make sure symbols were applied
context = QgsRenderContext()
renderer.startRender(context, QgsFields())
symbol, ok = renderer.symbolForValue2('a')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#ff0a0a')
symbol, ok = renderer.symbolForValue2('c ')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#0aff0a')
renderer.stopRender(context)
# tolerant match, case insensitive
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, False, True)
self.assertEqual(matched, 3)
self.assertFalse(unmatched_cats)
self.assertEqual(unmatched_symbols, ['C', 'b'])
# make sure symbols were applied
context = QgsRenderContext()
renderer.startRender(context, QgsFields())
symbol, ok = renderer.symbolForValue2('a')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#ff0a0a')
symbol, ok = renderer.symbolForValue2('b')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#0aff0a')
symbol, ok = renderer.symbolForValue2('c ')
self.assertTrue(ok)
self.assertEqual(symbol.color().name(), '#0aff0a')
renderer.stopRender(context)
if __name__ == "__main__":
unittest.main()