Skip to content

Commit 97a964a

Browse files
committedSep 10, 2018
Add method QgsCategorizedSymbolRenderer::matchToSymbols which
matches existing categories to symbol names from a QgsStyle object and copies matching symbols to these categories
1 parent 97c9580 commit 97a964a

File tree

5 files changed

+259
-15
lines changed

5 files changed

+259
-15
lines changed
 

‎python/core/auto_generated/symbology/qgscategorizedsymbolrenderer.sip.in

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,26 @@ Returns configuration of appearance of legend when using data-defined size for m
255255
Will return null if the functionality is disabled.
256256

257257
.. versionadded:: 3.0
258+
%End
259+
260+
int matchToSymbols( QgsStyle *style, QgsSymbol::SymbolType type,
261+
QVariantList &unmatchedCategories /Out/, QStringList &unmatchedSymbols /Out/, bool caseSensitive = true, bool useTolerantMatch = false );
262+
%Docstring
263+
Replaces category symbols with the symbols from a ``style`` that have a matching
264+
name and symbol ``type``.
265+
266+
The ``unmatchedCategories`` list will be filled with all existing categories which could not be matched
267+
to a symbol in ``style``.
268+
269+
The ``unmatchedSymbols`` list will be filled with all symbol names from ``style`` which were not be matched
270+
to an existing category.
271+
272+
If ``caseSensitive`` is false, then a case-insensitive match will be performed. If ``useTolerantMatch``
273+
is true, then non-alphanumeric characters in style and category names will be ignored during the match.
274+
275+
Returns the count of symbols matched.
276+
277+
.. versionadded:: 3.4
258278
%End
259279

260280
protected:

‎src/core/symbology/qgscategorizedsymbolrenderer.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "qgsvectorlayer.h"
3030
#include "qgslogger.h"
3131
#include "qgsproperty.h"
32+
#include "qgsstyle.h"
3233

3334
#include <QDomDocument>
3435
#include <QDomElement>
@@ -956,3 +957,65 @@ QgsDataDefinedSizeLegend *QgsCategorizedSymbolRenderer::dataDefinedSizeLegend()
956957
{
957958
return mDataDefinedSizeLegend.get();
958959
}
960+
961+
int QgsCategorizedSymbolRenderer::matchToSymbols( QgsStyle *style, const QgsSymbol::SymbolType type, QVariantList &unmatchedCategories, QStringList &unmatchedSymbols, const bool caseSensitive, const bool useTolerantMatch )
962+
{
963+
if ( !style )
964+
return 0;
965+
966+
int matched = 0;
967+
unmatchedSymbols = style->symbolNames();
968+
const QSet< QString > allSymbolNames = unmatchedSymbols.toSet();
969+
970+
const QRegularExpression tolerantMatchRe( QStringLiteral( "[^\\w\\d ]" ), QRegularExpression::UseUnicodePropertiesOption );
971+
972+
for ( int catIdx = 0; catIdx < mCategories.count(); ++catIdx )
973+
{
974+
const QVariant value = mCategories.at( catIdx ).value();
975+
const QString val = value.toString().trimmed();
976+
std::unique_ptr< QgsSymbol > symbol( style->symbol( val ) );
977+
// case-sensitive match
978+
if ( symbol && symbol->type() == type )
979+
{
980+
matched++;
981+
unmatchedSymbols.removeAll( val );
982+
updateCategorySymbol( catIdx, symbol.release() );
983+
continue;
984+
}
985+
986+
if ( !caseSensitive || useTolerantMatch )
987+
{
988+
QString testVal = val;
989+
if ( useTolerantMatch )
990+
testVal.replace( tolerantMatchRe, QString() );
991+
992+
bool foundMatch = false;
993+
for ( const QString &name : allSymbolNames )
994+
{
995+
QString testName = name.trimmed();
996+
if ( useTolerantMatch )
997+
testName.replace( tolerantMatchRe, QString() );
998+
999+
if ( testName == testVal || ( !caseSensitive && testName.trimmed().compare( testVal, Qt::CaseInsensitive ) == 0 ) )
1000+
{
1001+
// found a case-insensitive match
1002+
std::unique_ptr< QgsSymbol > symbol( style->symbol( name ) );
1003+
if ( symbol && symbol->type() == type )
1004+
{
1005+
matched++;
1006+
unmatchedSymbols.removeAll( name );
1007+
updateCategorySymbol( catIdx, symbol.release() );
1008+
foundMatch = true;
1009+
break;
1010+
}
1011+
}
1012+
}
1013+
if ( foundMatch )
1014+
continue;
1015+
}
1016+
1017+
unmatchedCategories << value;
1018+
}
1019+
1020+
return matched;
1021+
}

‎src/core/symbology/qgscategorizedsymbolrenderer.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <QHash>
2727

2828
class QgsVectorLayer;
29+
class QgsStyle;
2930

3031
/**
3132
* \ingroup core
@@ -225,6 +226,26 @@ class CORE_EXPORT QgsCategorizedSymbolRenderer : public QgsFeatureRenderer
225226
*/
226227
QgsDataDefinedSizeLegend *dataDefinedSizeLegend() const;
227228

229+
/**
230+
* Replaces category symbols with the symbols from a \a style that have a matching
231+
* name and symbol \a type.
232+
*
233+
* The \a unmatchedCategories list will be filled with all existing categories which could not be matched
234+
* to a symbol in \a style.
235+
*
236+
* The \a unmatchedSymbols list will be filled with all symbol names from \a style which were not be matched
237+
* to an existing category.
238+
*
239+
* If \a caseSensitive is false, then a case-insensitive match will be performed. If \a useTolerantMatch
240+
* is true, then non-alphanumeric characters in style and category names will be ignored during the match.
241+
*
242+
* Returns the count of symbols matched.
243+
*
244+
* \since QGIS 3.4
245+
*/
246+
int matchToSymbols( QgsStyle *style, QgsSymbol::SymbolType type,
247+
QVariantList &unmatchedCategories SIP_OUT, QStringList &unmatchedSymbols SIP_OUT, bool caseSensitive = true, bool useTolerantMatch = false );
248+
228249
protected:
229250
QString mAttrName;
230251
QgsCategoryList mCategories;

‎src/gui/symbology/qgscategorizedsymbolrendererwidget.cpp

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -921,20 +921,14 @@ int QgsCategorizedSymbolRendererWidget::matchToSymbols( QgsStyle *style )
921921
if ( !mLayer || !style )
922922
return 0;
923923

924-
int matched = 0;
925-
for ( int catIdx = 0; catIdx < mRenderer->categories().count(); ++catIdx )
926-
{
927-
QString val = mRenderer->categories().at( catIdx ).value().toString();
928-
std::unique_ptr< QgsSymbol > symbol( style->symbol( val ) );
929-
if ( symbol &&
930-
( ( symbol->type() == QgsSymbol::Marker && mLayer->geometryType() == QgsWkbTypes::PointGeometry )
931-
|| ( symbol->type() == QgsSymbol::Line && mLayer->geometryType() == QgsWkbTypes::LineGeometry )
932-
|| ( symbol->type() == QgsSymbol::Fill && mLayer->geometryType() == QgsWkbTypes::PolygonGeometry ) ) )
933-
{
934-
matched++;
935-
mRenderer->updateCategorySymbol( catIdx, symbol.release() );
936-
}
937-
}
924+
const QgsSymbol::SymbolType type = mLayer->geometryType() == QgsWkbTypes::PointGeometry ? QgsSymbol::Marker
925+
: mLayer->geometryType() == QgsWkbTypes::LineGeometry ? QgsSymbol::Line
926+
: QgsSymbol::Fill;
927+
928+
QVariantList unmatchedCategories;
929+
QStringList unmatchedSymbols;
930+
const int matched = mRenderer->matchToSymbols( style, type, unmatchedCategories, unmatchedSymbols );
931+
938932
mModel->updateSymbology();
939933
return matched;
940934
}

‎tests/src/python/test_qgscategorizedsymbolrenderer.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
from qgis.core import (QgsCategorizedSymbolRenderer,
1919
QgsRendererCategory,
2020
QgsMarkerSymbol,
21+
QgsLineSymbol,
22+
QgsFillSymbol,
2123
QgsField,
2224
QgsFields,
2325
QgsFeature,
24-
QgsRenderContext
26+
QgsRenderContext,
27+
QgsSymbol,
28+
QgsStyle
2529
)
2630
from qgis.PyQt.QtCore import QVariant
2731
from qgis.PyQt.QtGui import QColor
@@ -38,6 +42,20 @@ def createMarkerSymbol():
3842
return symbol
3943

4044

45+
def createLineSymbol():
46+
symbol = QgsLineSymbol.createSimple({
47+
"color": "100,150,50"
48+
})
49+
return symbol
50+
51+
52+
def createFillSymbol():
53+
symbol = QgsFillSymbol.createSimple({
54+
"color": "100,150,50"
55+
})
56+
return symbol
57+
58+
4159
class TestQgsCategorizedSymbolRenderer(unittest.TestCase):
4260

4361
def testFilter(self):
@@ -312,6 +330,134 @@ def testLegendKeysWhileCounting(self):
312330

313331
renderer.stopRender(context)
314332

333+
def testMatchToSymbols(self):
334+
"""
335+
Test QgsCategorizedSymbolRender.matchToSymbols
336+
"""
337+
renderer = QgsCategorizedSymbolRenderer()
338+
renderer.setClassAttribute('x')
339+
340+
symbol_a = createMarkerSymbol()
341+
symbol_a.setColor(QColor(255, 0, 0))
342+
renderer.addCategory(QgsRendererCategory('a', symbol_a, 'a'))
343+
symbol_b = createMarkerSymbol()
344+
symbol_b.setColor(QColor(0, 255, 0))
345+
renderer.addCategory(QgsRendererCategory('b', symbol_b, 'b'))
346+
symbol_c = createMarkerSymbol()
347+
symbol_c.setColor(QColor(0, 0, 255))
348+
renderer.addCategory(QgsRendererCategory('c ', symbol_c, 'c'))
349+
350+
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(None, QgsSymbol.Marker)
351+
self.assertEqual(matched, 0)
352+
353+
style = QgsStyle()
354+
symbol_a = createMarkerSymbol()
355+
symbol_a.setColor(QColor(255, 10, 10))
356+
self.assertTrue(style.addSymbol('a', symbol_a))
357+
symbol_B = createMarkerSymbol()
358+
symbol_B.setColor(QColor(10, 255, 10))
359+
self.assertTrue(style.addSymbol('B ', symbol_B))
360+
symbol_b = createFillSymbol()
361+
symbol_b.setColor(QColor(10, 255, 10))
362+
self.assertTrue(style.addSymbol('b', symbol_b))
363+
symbol_C = createLineSymbol()
364+
symbol_C.setColor(QColor(10, 255, 10))
365+
self.assertTrue(style.addSymbol('C', symbol_C))
366+
symbol_C = createMarkerSymbol()
367+
symbol_C.setColor(QColor(10, 255, 10))
368+
self.assertTrue(style.addSymbol(' ----c/- ', symbol_C))
369+
370+
# non-matching symbol type
371+
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Line)
372+
self.assertEqual(matched, 0)
373+
self.assertEqual(unmatched_cats, ['a', 'b', 'c '])
374+
self.assertEqual(unmatched_symbols, [' ----c/- ', 'B ', 'C', 'a', 'b'])
375+
376+
# exact match
377+
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker)
378+
self.assertEqual(matched, 1)
379+
self.assertEqual(unmatched_cats, ['b', 'c '])
380+
self.assertEqual(unmatched_symbols, [' ----c/- ', 'B ', 'C', 'b'])
381+
382+
# make sure symbol was applied
383+
context = QgsRenderContext()
384+
renderer.startRender(context, QgsFields())
385+
symbol, ok = renderer.symbolForValue2('a')
386+
self.assertTrue(ok)
387+
self.assertEqual(symbol.color().name(), '#ff0a0a')
388+
renderer.stopRender(context)
389+
390+
# case insensitive match
391+
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, False)
392+
self.assertEqual(matched, 2)
393+
self.assertEqual(unmatched_cats, ['c '])
394+
self.assertEqual(unmatched_symbols, [' ----c/- ', 'C', 'b'])
395+
396+
# make sure symbols were applied
397+
context = QgsRenderContext()
398+
renderer.startRender(context, QgsFields())
399+
symbol, ok = renderer.symbolForValue2('a')
400+
self.assertTrue(ok)
401+
self.assertEqual(symbol.color().name(), '#ff0a0a')
402+
symbol, ok = renderer.symbolForValue2('b')
403+
self.assertTrue(ok)
404+
self.assertEqual(symbol.color().name(), '#0aff0a')
405+
renderer.stopRender(context)
406+
407+
# case insensitive match
408+
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, False)
409+
self.assertEqual(matched, 2)
410+
self.assertEqual(unmatched_cats, ['c '])
411+
self.assertEqual(unmatched_symbols, [' ----c/- ', 'C', 'b'])
412+
413+
# make sure symbols were applied
414+
context = QgsRenderContext()
415+
renderer.startRender(context, QgsFields())
416+
symbol, ok = renderer.symbolForValue2('a')
417+
self.assertTrue(ok)
418+
self.assertEqual(symbol.color().name(), '#ff0a0a')
419+
symbol, ok = renderer.symbolForValue2('b')
420+
self.assertTrue(ok)
421+
self.assertEqual(symbol.color().name(), '#0aff0a')
422+
renderer.stopRender(context)
423+
424+
# tolerant match
425+
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, True, True)
426+
self.assertEqual(matched, 2)
427+
self.assertEqual(unmatched_cats, ['b'])
428+
self.assertEqual(unmatched_symbols, ['B ', 'C', 'b'])
429+
430+
# make sure symbols were applied
431+
context = QgsRenderContext()
432+
renderer.startRender(context, QgsFields())
433+
symbol, ok = renderer.symbolForValue2('a')
434+
self.assertTrue(ok)
435+
self.assertEqual(symbol.color().name(), '#ff0a0a')
436+
symbol, ok = renderer.symbolForValue2('c ')
437+
self.assertTrue(ok)
438+
self.assertEqual(symbol.color().name(), '#0aff0a')
439+
renderer.stopRender(context)
440+
441+
# tolerant match, case insensitive
442+
matched, unmatched_cats, unmatched_symbols = renderer.matchToSymbols(style, QgsSymbol.Marker, False, True)
443+
self.assertEqual(matched, 3)
444+
self.assertFalse(unmatched_cats)
445+
self.assertEqual(unmatched_symbols, ['C', 'b'])
446+
447+
# make sure symbols were applied
448+
context = QgsRenderContext()
449+
renderer.startRender(context, QgsFields())
450+
symbol, ok = renderer.symbolForValue2('a')
451+
self.assertTrue(ok)
452+
self.assertEqual(symbol.color().name(), '#ff0a0a')
453+
symbol, ok = renderer.symbolForValue2('b')
454+
self.assertTrue(ok)
455+
self.assertEqual(symbol.color().name(), '#0aff0a')
456+
symbol, ok = renderer.symbolForValue2('c ')
457+
self.assertTrue(ok)
458+
self.assertEqual(symbol.color().name(), '#0aff0a')
459+
renderer.stopRender(context)
460+
315461

316462
if __name__ == "__main__":
317463
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.