Skip to content

Commit 785d995

Browse files
committed
Add a bunch of geometry unit tests against reference geometries
(inc geometry count, node count, ring count, area, centroid, x/y min/max) TODO: - invesigate whether Z/M is required for GeometryCollection WKT data type if children have Z/M dimension - fix QGIS handling of mixing length and perimeter for collections resulting in incorrect length calculation for mixed Collections
1 parent fab5a32 commit 785d995

File tree

3 files changed

+176
-17
lines changed

3 files changed

+176
-17
lines changed

tests/src/python/test_qgsgeometry.py

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import qgis
1616
import os
17+
import csv
1718

1819
from qgis.core import (QgsGeometry,
1920
QgsVectorLayer,
@@ -28,6 +29,7 @@
2829
TestCase,
2930
unittest,
3031
compareWkt,
32+
doubleNear,
3133
unitTestDataPath,
3234
writeShape)
3335

@@ -116,22 +118,78 @@ def testFromMultiPolygon(self):
116118
(QGis.WKBMultiPolygon, myMultiPolygon.type()))
117119
assert myMultiPolygon.wkbType() == QGis.WKBMultiPolygon, myMessage
118120

119-
def testExportToWkt(self):
120-
""" Test parsing a whole range of valid wkt formats and variants.
121+
def testReferenceGeometry(self):
122+
""" Test parsing a whole range of valid reference wkt formats and variants, and checking
123+
expected values such as length, area, centroids, bounding boxes, etc of the resultant geometry.
121124
Note the bulk of this test data was taken from the PostGIS WKT test data """
122-
with open(os.path.join(TEST_DATA_DIR, 'wkt_data.csv'), 'r') as d:
123-
for i, t in enumerate(d):
124-
test_data = t.strip().split('|')
125-
wkt = test_data[0].strip()
126-
geom = QgsGeometry.fromWkt(wkt)
127-
assert geom, "WKT conversion {} failed: could not create geom:\n{}\n".format(i + 1, wkt)
125+
126+
with open(os.path.join(TEST_DATA_DIR, 'geom_data.csv'), 'rb') as f:
127+
reader = csv.DictReader(f)
128+
for i, row in enumerate(reader):
129+
130+
#test that geometry can be created from WKT
131+
geom = QgsGeometry.fromWkt(row['wkt'])
132+
assert geom, "WKT conversion {} failed: could not create geom:\n{}\n".format(i + 1, row['wkt'])
133+
134+
#test exporting to WKT results in expected string
128135
result = geom.exportToWkt()
129-
if len(test_data) > 1:
130-
exp = test_data[1]
131-
else:
132-
exp = test_data[0]
136+
exp = row['valid_wkt']
133137
assert compareWkt(result, exp), "WKT conversion {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
134138

139+
#test num points in geometry
140+
exp_nodes = int(row['num_points'])
141+
assert geom.geometry().nCoordinates() == exp_nodes, "Node count {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp_nodes, geom.geometry().nCoordinates())
142+
143+
#test num geometries in collections
144+
exp_geometries = int(row['num_geometries'])
145+
try:
146+
assert geom.geometry().numGeometries() == exp_geometries, "Geometry count {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp_geometries, geom.geometry().numGeometries())
147+
except:
148+
#some geometry types don't have numGeometries()
149+
assert exp_geometries <= 1, "Geometry count {}: Expected:\n{} geometries but could not call numGeometries()\n".format(i + 1, exp_geometries)
150+
151+
#test count of rings
152+
if row['num_rings']:
153+
exp_rings = int(row['num_rings'])
154+
try:
155+
assert geom.geometry().numInteriorRings() == exp_geometries, "Ring count {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp_geometries, geom.geometry().numInteriorRings())
156+
except:
157+
#some geometry types don't have numInteriorRings()
158+
assert exp_geometries <= 1, "Ring count {}: Expected:\n{} rings but could not call numInteriorRings()\n".format(i + 1, exp_geometries)
159+
160+
#test geometry centroid
161+
exp = row['centroid']
162+
result = geom.centroid().exportToWkt()
163+
assert compareWkt(result, exp), "Centroid {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
164+
165+
#test bounding box limits
166+
bbox = geom.geometry().boundingBox()
167+
exp = float(row['x_min'])
168+
result = bbox.xMinimum()
169+
assert doubleNear(result, exp), "Min X {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
170+
exp = float(row['y_min'])
171+
result = bbox.yMinimum()
172+
assert doubleNear(result, exp), "Min Y {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
173+
exp = float(row['x_max'])
174+
result = bbox.xMaximum()
175+
assert doubleNear(result, exp), "Max X {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
176+
exp = float(row['y_max'])
177+
result = bbox.yMaximum()
178+
assert doubleNear(result, exp), "Max Y {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
179+
180+
#test area calculation
181+
exp = float(row['area'])
182+
result = geom.geometry().area()
183+
assert doubleNear(result, exp), "Area {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
184+
185+
#NOTE - disabled due to misleading length/perimeter calculations for geometry collections
186+
#exp = float(row['length'])
187+
#result = geom.geometry().length()
188+
#assert doubleNear(result, exp), "Length {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
189+
#exp = float(row['perimeter'])
190+
#result = geom.geometry().length()
191+
#assert doubleNear(result, exp), "Length {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
192+
135193
def testArea(self):
136194
""" Test area calculations """
137195
with open(os.path.join(TEST_DATA_DIR, 'area_data.csv'), 'r') as d:

tests/src/python/utilities.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,13 +205,43 @@ def writeShape(theMemoryLayer, theFileName):
205205
assert myResult == QgsVectorFileWriter.NoError
206206

207207

208+
def doubleNear(a, b, tol=0.0000000001):
209+
"""
210+
Tests whether two floats are near, within a specified tolerance
211+
"""
212+
return abs(float(a) - float(b)) < tol
213+
214+
208215
def compareWkt(a, b, tol=0.000001):
209-
r0 = re.compile("-?\d+(?:\.\d+)?(?:[eE]\d+)?")
210-
r1 = re.compile("\s*,\s*")
216+
"""
217+
Compares two WKT strings, ignoring allowed differences between strings
218+
and allowing a tolerance for coordinates
219+
"""
220+
#ignore case
221+
a0 = a.lower()
222+
b0 = b.lower()
223+
224+
#remove optional spaces before z/m
225+
r = re.compile("\s+([zm])")
226+
a0 = r.sub(r'\1', a0)
227+
b0 = r.sub(r'\1', b0)
228+
229+
#ignore the z/m flag on GeometryCollections
230+
#NOTE - I'm not sure about this, possibly the flag is required and there's a bug in QGIS omitting this
231+
r = re.compile("geometrycollection\s*[zm]*")
232+
a0 = r.sub('geometrycollection', a0)
233+
b0 = r.sub('geometrycollection', b0)
234+
235+
#spaces before brackets are optional
236+
r = re.compile("\s*\(")
237+
a0 = r.sub('(', a0)
238+
b0 = r.sub('(', b0)
211239

212240
# compare the structure
213-
a0 = r1.sub(",", r0.sub("#", a))
214-
b0 = r1.sub(",", r0.sub("#", b))
241+
r0 = re.compile("-?\d+(?:\.\d+)?(?:[eE]\d+)?")
242+
r1 = re.compile("\s*,\s*")
243+
a0 = r1.sub(",", r0.sub("#", a0))
244+
b0 = r1.sub(",", r0.sub("#", b0))
215245
if a0 != b0:
216246
return False
217247

@@ -222,7 +252,7 @@ def compareWkt(a, b, tol=0.000001):
222252
return False
223253

224254
for (a1, b1) in izip(a0, b0):
225-
if abs(float(a1) - float(b1)) > tol:
255+
if not doubleNear(a1, b1, tol):
226256
return False
227257

228258
return True

0 commit comments

Comments
 (0)