Skip to content

Commit 422ea56

Browse files
committedOct 19, 2014
Merge pull request #1630 from ccrook/GraduatedRendererUnitTests2
Graduated renderer unit tests2
2 parents 7798bd8 + 359a3d8 commit 422ea56

File tree

3 files changed

+501
-1
lines changed

3 files changed

+501
-1
lines changed
 

‎src/core/symbology-ng/qgsgraduatedsymbolrendererv2.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1113,10 +1113,11 @@ QgsFeatureRendererV2* QgsGraduatedSymbolRendererV2::create( QDomElement& element
11131113
double upperValue = rangeElem.attribute( "upper" ).toDouble();
11141114
QString symbolName = rangeElem.attribute( "symbol" );
11151115
QString label = rangeElem.attribute( "label" );
1116+
bool render=rangeElem.attribute("render","true") != "false";
11161117
if ( symbolMap.contains( symbolName ) )
11171118
{
11181119
QgsSymbolV2* symbol = symbolMap.take( symbolName );
1119-
ranges.append( QgsRendererRangeV2( lowerValue, upperValue, symbol, label ) );
1120+
ranges.append( QgsRendererRangeV2( lowerValue, upperValue, symbol, label, render ) );
11201121
}
11211122
}
11221123
rangeElem = rangeElem.nextSiblingElement();

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ ADD_PYTHON_TEST(PyQgsSpatialiteProvider test_qgsspatialiteprovider.py)
4040
ADD_PYTHON_TEST(PyQgsZonalStatistics test_qgszonalstatistics.py)
4141
ADD_PYTHON_TEST(PyQgsAppStartup test_qgsappstartup.py)
4242
ADD_PYTHON_TEST(PyQgsDistanceArea test_qgsdistancearea.py)
43+
ADD_PYTHON_TEST(PyQgsGraduatedSymbolRendererV2 test_qgsgraduatedsymbolrendererv2.py)
Lines changed: 498 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,498 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsGraduatedSymbolRendererV2
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Chris Crook'
10+
__date__ = '3/10/2014'
11+
__copyright__ = 'Copyright 2014, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import qgis
16+
from utilities import (
17+
unittest,
18+
TestCase,
19+
getQgisTestApp,
20+
)
21+
from qgis.core import (
22+
QgsGraduatedSymbolRendererV2,
23+
QgsRendererRangeV2,
24+
QgsRendererRangeV2LabelFormat,
25+
QgsMarkerSymbolV2,
26+
QgsVectorGradientColorRampV2,
27+
QgsVectorLayer,
28+
QgsFeature,
29+
QgsGeometry,
30+
QgsPoint,
31+
QgsSymbolV2,
32+
QgsSymbolLayerV2Utils,
33+
)
34+
from PyQt4.QtCore import (
35+
Qt,
36+
)
37+
from PyQt4.QtXml import (
38+
QDomDocument,
39+
)
40+
from PyQt4.QtGui import (
41+
QColor,
42+
)
43+
44+
QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()
45+
46+
#===========================================================
47+
# Utility functions
48+
49+
def createMarkerSymbol():
50+
symbol=QgsMarkerSymbolV2.createSimple({
51+
"color":"100,150,50",
52+
"name":"square",
53+
"size":"3.0"
54+
})
55+
return symbol
56+
57+
def createMemoryLayer(values):
58+
ml=QgsVectorLayer("Point?crs=epsg:4236&field=id:integer&field=value:double",
59+
"test_data", "memory")
60+
# Data as list of x, y, id, value
61+
assert ml.isValid()
62+
pr=ml.dataProvider()
63+
fields=pr.fields()
64+
for id, value in enumerate(values):
65+
x=id*10.0
66+
feat=QgsFeature(fields)
67+
feat['id']=id
68+
feat['value']=value
69+
g=QgsGeometry.fromPoint(QgsPoint(x,x))
70+
feat.setGeometry(g)
71+
pr.addFeatures( [feat] )
72+
ml.updateExtents()
73+
return ml
74+
75+
def createColorRamp():
76+
return QgsVectorGradientColorRampV2(
77+
QColor(255,0,0),
78+
QColor(0,0,255)
79+
)
80+
81+
def createLabelFormat():
82+
format=QgsRendererRangeV2LabelFormat()
83+
template="%1 - %2 metres"
84+
precision=5
85+
format.setFormat(template)
86+
format.setPrecision(precision)
87+
format.setTrimTrailingZeroes(True)
88+
return format
89+
90+
# Note: Dump functions are not designed for a user friendly dump, just
91+
# for a moderately compact representation of a rendererer that is independent
92+
# of the renderer source code and so appropriate for use in unit tests.
93+
94+
def dumpRangeBreaks( ranges ):
95+
return dumpRangeList(ranges,breaksOnly=True)
96+
97+
def dumpRangeLabels( ranges ):
98+
return dumpRangeList(ranges,labelsOnly=True)
99+
100+
def dumpLabelFormat( format ):
101+
return (
102+
':'+format.format()+
103+
':'+str(format.precision())+
104+
':'+str(format.trimTrailingZeroes())+
105+
':')
106+
107+
def dumpRangeList( rlist, breaksOnly=False,labelsOnly=False ):
108+
rstr='('
109+
format="{0:.4f}-{1:.4f}"
110+
if not breaksOnly:
111+
format=format+":{2}:{3}:{4}:"
112+
if labelsOnly:
113+
format='{2}'
114+
for r in rlist:
115+
rstr = rstr + format.format(
116+
r.lowerValue(),
117+
r.upperValue(),
118+
r.label(),
119+
r.symbol().dump(),
120+
r.renderState(),
121+
) + ","
122+
return rstr+')'
123+
124+
# Crude dump for deterministic ramp - just dumps colors at a range of values
125+
126+
def dumpColorRamp( ramp ):
127+
if ramp is None:
128+
return ':None:'
129+
rampstr=':'
130+
for x in (0.0,0.33,0.66,1.0):
131+
rampstr = rampstr+ramp.color(x).name()+':'
132+
return rampstr
133+
134+
def dumpGraduatedRenderer( r ):
135+
rstr=':'
136+
rstr=rstr+r.classAttribute()+':'
137+
rstr=rstr+str(r.mode())+':'
138+
symbol=r.sourceSymbol()
139+
if symbol is None:
140+
rstr=rstr+'None'+':'
141+
else:
142+
rstr=rstr+symbol.dump()+':'
143+
rstr=rstr+dumpColorRamp(r.sourceColorRamp())
144+
rstr=rstr+str(r.invertedColorRamp())+':'
145+
rstr=rstr+dumpRangeList(r.ranges())
146+
rstr=rstr+r.rotationField()+':'
147+
rstr=rstr+r.sizeScaleField()+':'
148+
rstr=rstr+str(r.scaleMethod())+':'
149+
return rstr
150+
151+
#=================================================================
152+
# Tests
153+
154+
class TestQgsGraduatedSymbolRendererV2(TestCase):
155+
156+
def testQgsRendererRangeV2_1(self):
157+
"""Test QgsRendererRangeV2 getter/setter functions"""
158+
range=QgsRendererRangeV2()
159+
self.assertTrue(range)
160+
lower=123.45
161+
upper=234.56
162+
label="Test label"
163+
symbol=createMarkerSymbol()
164+
range.setLowerValue(lower)
165+
self.assertEqual(range.lowerValue(),lower,"Lower value getter/setter failed")
166+
range.setUpperValue(upper)
167+
self.assertEqual(range.upperValue(),upper,"Upper value getter/setter failed")
168+
range.setLabel(label)
169+
self.assertEqual(range.label(),label,"Label getter/setter failed")
170+
range.setRenderState(True)
171+
self.assertTrue(range.renderState(),"Render state getter/setter failed")
172+
range.setRenderState(False)
173+
self.assertFalse(range.renderState(),"Render state getter/setter failed")
174+
range.setSymbol(symbol.clone())
175+
self.assertEqual(symbol.dump(),range.symbol().dump(),"Symbol getter/setter failed")
176+
range2=QgsRendererRangeV2(lower,upper,symbol.clone(),label,False)
177+
self.assertEqual(range2.lowerValue(),lower,"Lower value from constructor failed")
178+
self.assertEqual(range2.upperValue(),upper,"Upper value from constructor failed")
179+
self.assertEqual(range2.label(),label,"Label from constructor failed")
180+
self.assertEqual(range2.symbol().dump(),symbol.dump(),"Symbol from constructor failed")
181+
self.assertFalse(range2.renderState(),"Render state getter/setter failed")
182+
183+
def testQgsRendererRangeV2LabelFormat_1(self):
184+
"""Test QgsRendererRangeV2LabelFormat getter/setter functions"""
185+
format=QgsRendererRangeV2LabelFormat()
186+
self.assertTrue(format,"QgsRendererRangeV2LabelFomat construction failed")
187+
template="%1 - %2 metres"
188+
precision=5
189+
format.setFormat(template)
190+
self.assertEqual(format.format(),template,"Format getter/setter failed")
191+
format.setPrecision(precision)
192+
self.assertEqual(format.precision(),precision,"Precision getter/setter failed")
193+
format.setTrimTrailingZeroes(True)
194+
self.assertTrue(format.trimTrailingZeroes(),"TrimTrailingZeroes getter/setter failed")
195+
format.setTrimTrailingZeroes(False)
196+
self.assertFalse(format.trimTrailingZeroes(),"TrimTrailingZeroes getter/setter failed")
197+
minprecision=-6;
198+
maxprecision=15;
199+
self.assertEqual(QgsRendererRangeV2LabelFormat.MinPrecision,minprecision,"Minimum precision != -6")
200+
self.assertEqual(QgsRendererRangeV2LabelFormat.MaxPrecision,maxprecision,"Maximum precision != 15")
201+
format.setPrecision(-10)
202+
self.assertEqual(format.precision(),minprecision,"Minimum precision not enforced")
203+
format.setPrecision(20)
204+
self.assertEqual(format.precision(),maxprecision,"Maximum precision not enforced")
205+
206+
def testQgsRendererRangeV2LabelFormat_2(self):
207+
"""Test QgsRendererRangeV2LabelFormat number format"""
208+
format=QgsRendererRangeV2LabelFormat()
209+
# Tests have precision, trim, value, expected
210+
# (Note: Not sure what impact of locale is on these tests)
211+
tests=(
212+
(2,False,1.0,'1.00'),
213+
(2,True,1.0,'1'),
214+
(2,False,1.234,'1.23'),
215+
(2,True,1.234,'1.23'),
216+
(2,False,1.236,'1.24'),
217+
(2,False,-1.236,'-1.24'),
218+
(2,False,-0.004,'0.00'),
219+
(2,True,1.002,'1'),
220+
(2,True,1.006,'1.01'),
221+
(2,True,1.096,'1.1'),
222+
(3,True,1.096,'1.096'),
223+
(-2,True,1496.45,'1500'),
224+
(-2,True,149.45,'100'),
225+
(-2,True,79.45,'100'),
226+
(-2,True,49.45,'0'),
227+
(-2,True,-49.45,'0'),
228+
(-2,True,-149.45,'-100'),
229+
)
230+
for f in tests:
231+
precision,trim,value,expected=f
232+
format.setPrecision(precision)
233+
format.setTrimTrailingZeroes(trim)
234+
result=format.formatNumber(value)
235+
testname="{0}:{1}:{2}".format(precision,trim,value)
236+
self.assertEqual(result,expected,
237+
"Number format error {0}:{1}:{2} => {3}".format(
238+
precision,trim,value,result))
239+
240+
# Label tests - label format, expected result.
241+
# Labels will be evaluated with lower=1.23 upper=2.34, precision=2
242+
ltests=(
243+
("%1 - %2","1.23 - 2.34"),
244+
("%1","1.23"),
245+
("%2","2.34"),
246+
("%2%","2.34%"),
247+
("%1%1","1.231.23"),
248+
("from %1 to %2 metres","from 1.23 to 2.34 metres"),
249+
("from %2 to %1 metres","from 2.34 to 1.23 metres"),
250+
)
251+
format.setPrecision(2)
252+
format.setTrimTrailingZeroes(False)
253+
lower=1.232
254+
upper=2.339
255+
for t in ltests:
256+
label,expected=t
257+
format.setFormat(label)
258+
result=format.labelForLowerUpper(lower,upper)
259+
self.assertEqual(result,expected,"Label format error {0} => {1}".format(
260+
label,result))
261+
262+
range=QgsRendererRangeV2()
263+
range.setLowerValue(lower)
264+
range.setUpperValue(upper)
265+
label=ltests[0][0]
266+
format.setFormat(label)
267+
result=format.labelForRange(range)
268+
self.assertEqual(result,ltests[0][1],"Label for range error {0} => {1}".format(
269+
label,result))
270+
271+
def testQgsGraduatedSymbolRendererV2_1(self):
272+
"""Test QgsGraduatedSymbolRendererV2: Basic get/set functions """
273+
274+
# Create a renderer
275+
renderer=QgsGraduatedSymbolRendererV2()
276+
277+
symbol=createMarkerSymbol()
278+
renderer.setSourceSymbol(symbol.clone())
279+
self.assertEqual(symbol.dump(),renderer.sourceSymbol().dump(),"Get/set renderer source symbol")
280+
281+
attr='"value"*"value"'
282+
renderer.setClassAttribute(attr)
283+
self.assertEqual(attr,renderer.classAttribute(),"Get/set renderer class attribute")
284+
285+
for m in (
286+
QgsGraduatedSymbolRendererV2.Custom,
287+
QgsGraduatedSymbolRendererV2.EqualInterval,
288+
QgsGraduatedSymbolRendererV2.Quantile,
289+
QgsGraduatedSymbolRendererV2.Jenks,
290+
QgsGraduatedSymbolRendererV2.Pretty,
291+
QgsGraduatedSymbolRendererV2.StdDev,
292+
):
293+
renderer.setMode(m)
294+
self.assertEqual(m,renderer.mode(),"Get/set renderer mode")
295+
296+
format=createLabelFormat()
297+
renderer.setLabelFormat(format)
298+
self.assertEqual(
299+
dumpLabelFormat(format),
300+
dumpLabelFormat(renderer.labelFormat()),
301+
"Get/set renderer label format")
302+
303+
ramp=createColorRamp()
304+
renderer.setSourceColorRamp(ramp)
305+
self.assertEqual(
306+
dumpColorRamp(ramp),
307+
dumpColorRamp(renderer.sourceColorRamp()),
308+
"Get/set renderer color ramp")
309+
310+
renderer.setInvertedColorRamp(True)
311+
self.assertTrue(renderer.invertedColorRamp(),
312+
"Get/set renderer inverted color ramp")
313+
renderer.setInvertedColorRamp(False)
314+
self.assertFalse(renderer.invertedColorRamp(),
315+
"Get/set renderer inverted color ramp")
316+
317+
value='"value"*2'
318+
exp=QgsSymbolLayerV2Utils.fieldOrExpressionToExpression(value)
319+
valuestr=QgsSymbolLayerV2Utils.fieldOrExpressionFromExpression(exp)
320+
renderer.setRotationField(value)
321+
self.assertEqual(valuestr,renderer.rotationField(),
322+
"Get/set renderer rotation field")
323+
324+
value='"value"*3'
325+
exp=QgsSymbolLayerV2Utils.fieldOrExpressionToExpression(value)
326+
valuestr=QgsSymbolLayerV2Utils.fieldOrExpressionFromExpression(exp)
327+
renderer.setSizeScaleField(value)
328+
self.assertEqual(valuestr,renderer.sizeScaleField(),
329+
"Get/set renderer size scale field")
330+
331+
332+
renderer.setSourceColorRamp(ramp)
333+
self.assertEqual(
334+
dumpColorRamp(ramp),
335+
dumpColorRamp(renderer.sourceColorRamp()),
336+
"Get/set renderer color ramp")
337+
338+
for sm in (
339+
QgsSymbolV2.ScaleArea,
340+
QgsSymbolV2.ScaleDiameter,
341+
):
342+
renderer.setScaleMethod(sm)
343+
self.assertEqual(str(sm),str(renderer.scaleMethod()),
344+
"Get/set renderer scale method")
345+
346+
347+
348+
def testQgsGraduatedSymbolRendererV2_2(self):
349+
"""Test QgsGraduatedSymbolRendererV2: Adding /removing/editing classes """
350+
# Create a renderer
351+
renderer=QgsGraduatedSymbolRendererV2()
352+
symbol=createMarkerSymbol()
353+
renderer.setSourceSymbol(symbol.clone())
354+
symbol.setColor(QColor(255,0,0))
355+
356+
# Add class without start and end ranges
357+
358+
renderer.addClass(symbol.clone())
359+
renderer.addClass(symbol.clone())
360+
renderer.updateRangeLabel(1,'Second range')
361+
renderer.updateRangeLowerValue(1,10.0)
362+
renderer.updateRangeUpperValue(1,25.0)
363+
renderer.updateRangeRenderState(1,False)
364+
symbol.setColor(QColor(0,0,255))
365+
renderer.updateRangeSymbol(1,symbol.clone())
366+
367+
# Add as a rangeobject
368+
symbol.setColor(QColor(0,255,0))
369+
range=QgsRendererRangeV2(20.0,25.5,symbol.clone(),'Third range',False)
370+
renderer.addClassRange(range)
371+
372+
# Add class by lower and upper
373+
renderer.addClassLowerUpper(25.5,30.5)
374+
# (Update label for sorting tests)
375+
renderer.updateRangeLabel(3,'Another range')
376+
377+
rangeListStr=dumpRangeList(renderer.ranges())
378+
self.assertEqual(
379+
dumpRangeLabels(renderer.ranges()),
380+
'(0.0 - 0.0,Second range,Third range,Another range,)',
381+
'Added ranges labels not correct')
382+
self.assertEqual(
383+
dumpRangeBreaks(renderer.ranges()),
384+
'(0.0000-0.0000,10.0000-25.0000,20.0000-25.5000,25.5000-30.5000,)',
385+
'Added ranges lower/upper values not correct')
386+
387+
# Check that clone function works
388+
389+
renderer2=renderer.clone()
390+
self.assertEqual(
391+
dumpGraduatedRenderer(renderer),
392+
dumpGraduatedRenderer(renderer2),
393+
"clone function doesn't replicate renderer properly"
394+
)
395+
396+
# Check save and reload from Dom works
397+
398+
doc=QDomDocument()
399+
element=renderer.save(doc)
400+
renderer2=QgsGraduatedSymbolRendererV2.create(element)
401+
self.assertEqual(
402+
dumpGraduatedRenderer(renderer),
403+
dumpGraduatedRenderer(renderer2),
404+
"Save/create from DOM doesn't replicate renderer properly"
405+
)
406+
407+
# Check sorting
408+
409+
renderer.sortByLabel()
410+
self.assertEqual(
411+
dumpRangeList(renderer.ranges(),labelsOnly=True),
412+
'(0.0 - 0.0,Another range,Second range,Third range,)',
413+
'sortByLabel not correct')
414+
renderer.sortByValue()
415+
self.assertEqual(
416+
dumpRangeBreaks(renderer.ranges()),
417+
'(0.0000-0.0000,10.0000-25.0000,20.0000-25.5000,25.5000-30.5000,)',
418+
'sortByValue not correct')
419+
renderer.sortByValue(Qt.DescendingOrder)
420+
self.assertEqual(
421+
dumpRangeBreaks(renderer.ranges()),
422+
'(25.5000-30.5000,20.0000-25.5000,10.0000-25.0000,0.0000-0.0000,)',
423+
'sortByValue descending not correct')
424+
425+
# Check deleting
426+
427+
renderer.deleteClass(2)
428+
self.assertEqual(
429+
dumpRangeBreaks(renderer.ranges()),
430+
'(25.5000-30.5000,20.0000-25.5000,0.0000-0.0000,)',
431+
'deleteClass not correct')
432+
433+
renderer.deleteAllClasses()
434+
self.assertEqual(len(renderer.ranges()),0,"deleteAllClasses didn't delete all")
435+
436+
437+
# void addClass( QgsSymbolV2* symbol );
438+
# //! @note available in python bindings as addClassRange
439+
# void addClass( QgsRendererRangeV2 range ) /PyName=addClassRange/;
440+
# //! @note available in python bindings as addClassLowerUpper
441+
# void addClass( double lower, double upper ) /PyName=addClassLowerUpper/;
442+
# void deleteClass( int idx );
443+
# void deleteAllClasses();
444+
445+
def testQgsGraduatedSymbolRendererV2_3(self):
446+
"""Test QgsGraduatedSymbolRendererV2: Reading attribute data, calculating classes """
447+
448+
# Create a renderer
449+
renderer=QgsGraduatedSymbolRendererV2()
450+
symbol=createMarkerSymbol()
451+
renderer.setSourceSymbol(symbol.clone())
452+
453+
454+
455+
# Test retrieving data values from a layer
456+
ml=createMemoryLayer((1.2,0.5,5.0,1.0,1.0,1.2))
457+
# ... by attribute
458+
renderer.setClassAttribute("value")
459+
self.assertEqual(renderer.classAttribute(),"value","Error in set/get classAttribute")
460+
data=renderer.getDataValues(ml)
461+
datastr=':'.join([str(x) for x in data])
462+
self.assertEqual(datastr,'1.2:0.5:5.0:1.0:1.0:1.2',"Error returning field data")
463+
# ... by expression
464+
renderer.setClassAttribute('"value"*"value"')
465+
self.assertEqual(renderer.classAttribute(),'"value"*"value"',"Error in set/get classAttribute")
466+
data=renderer.getDataValues(ml)
467+
datastr=':'.join([str(x) for x in data])
468+
self.assertEqual(datastr,'1.44:0.25:25.0:1.0:1.0:1.44',"Error returning field expression")
469+
470+
471+
renderer.setClassAttribute("value")
472+
# Equal interval calculations
473+
renderer.updateClasses(ml,renderer.EqualInterval,3)
474+
self.assertEqual(
475+
dumpRangeBreaks(renderer.ranges()),
476+
'(0.5000-2.0000,2.0000-3.5000,3.5000-5.0000,)',
477+
'Equal interval classification not correct')
478+
479+
# Quantile classes
480+
renderer.updateClasses(ml,renderer.Quantile,3)
481+
self.assertEqual(
482+
dumpRangeBreaks(renderer.ranges()),
483+
'(0.5000-1.0000,1.0000-1.2000,1.2000-5.0000,)',
484+
'Quantile classification not correct')
485+
renderer.updateClasses(ml,renderer.Quantile,4)
486+
self.assertEqual(
487+
dumpRangeBreaks(renderer.ranges()),
488+
'(0.5000-1.0000,1.0000-1.1000,1.1000-1.2000,1.2000-5.0000,)',
489+
'Quantile classification not correct')
490+
491+
# Tests still needed
492+
493+
# Other calculation method tests
494+
# createRenderer function
495+
# symbolForFeature correctly selects range
496+
497+
if __name__ == "__main__":
498+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.