Skip to content

Commit

Permalink
Add method QgsCategorizedSymbolRenderer::matchToSymbols which
Browse files Browse the repository at this point in the history
matches existing categories to symbol names from a QgsStyle
object and copies matching symbols to these categories
  • Loading branch information
nyalldawson committed Sep 10, 2018
1 parent 97c9580 commit 97a964a
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
63 changes: 63 additions & 0 deletions src/core/symbology/qgscategorizedsymbolrenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "qgsvectorlayer.h"
#include "qgslogger.h"
#include "qgsproperty.h"
#include "qgsstyle.h"

#include <QDomDocument>
#include <QDomElement>
Expand Down Expand Up @@ -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;
}
21 changes: 21 additions & 0 deletions src/core/symbology/qgscategorizedsymbolrenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <QHash>

class QgsVectorLayer;
class QgsStyle;

/**
* \ingroup core
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 8 additions & 14 deletions src/gui/symbology/qgscategorizedsymbolrendererwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
148 changes: 147 additions & 1 deletion tests/src/python/test_qgscategorizedsymbolrenderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
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
Expand All @@ -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):
Expand Down Expand Up @@ -312,6 +330,134 @@ def testLegendKeysWhileCounting(self):

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()

0 comments on commit 97a964a

Please sign in to comment.