Skip to content
Permalink
Browse files

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
  • Loading branch information
nyalldawson committed Oct 17, 2015
1 parent fab5a32 commit 785d9952a0a5619d1edc8b234d13ebfa08f02665
Showing with 176 additions and 17 deletions.
  1. +70 −12 tests/src/python/test_qgsgeometry.py
  2. +35 −5 tests/src/python/utilities.py
  3. +71 −0 tests/testdata/geom_data.csv
@@ -14,6 +14,7 @@

import qgis
import os
import csv

from qgis.core import (QgsGeometry,
QgsVectorLayer,
@@ -28,6 +29,7 @@
TestCase,
unittest,
compareWkt,
doubleNear,
unitTestDataPath,
writeShape)

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

def testExportToWkt(self):
""" Test parsing a whole range of valid wkt formats and variants.
def testReferenceGeometry(self):
""" Test parsing a whole range of valid reference wkt formats and variants, and checking
expected values such as length, area, centroids, bounding boxes, etc of the resultant geometry.
Note the bulk of this test data was taken from the PostGIS WKT test data """
with open(os.path.join(TEST_DATA_DIR, 'wkt_data.csv'), 'r') as d:
for i, t in enumerate(d):
test_data = t.strip().split('|')
wkt = test_data[0].strip()
geom = QgsGeometry.fromWkt(wkt)
assert geom, "WKT conversion {} failed: could not create geom:\n{}\n".format(i + 1, wkt)

with open(os.path.join(TEST_DATA_DIR, 'geom_data.csv'), 'rb') as f:
reader = csv.DictReader(f)
for i, row in enumerate(reader):

#test that geometry can be created from WKT
geom = QgsGeometry.fromWkt(row['wkt'])
assert geom, "WKT conversion {} failed: could not create geom:\n{}\n".format(i + 1, row['wkt'])

#test exporting to WKT results in expected string
result = geom.exportToWkt()
if len(test_data) > 1:
exp = test_data[1]
else:
exp = test_data[0]
exp = row['valid_wkt']
assert compareWkt(result, exp), "WKT conversion {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)

#test num points in geometry
exp_nodes = int(row['num_points'])
assert geom.geometry().nCoordinates() == exp_nodes, "Node count {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp_nodes, geom.geometry().nCoordinates())

#test num geometries in collections
exp_geometries = int(row['num_geometries'])
try:
assert geom.geometry().numGeometries() == exp_geometries, "Geometry count {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp_geometries, geom.geometry().numGeometries())
except:
#some geometry types don't have numGeometries()
assert exp_geometries <= 1, "Geometry count {}: Expected:\n{} geometries but could not call numGeometries()\n".format(i + 1, exp_geometries)

#test count of rings
if row['num_rings']:
exp_rings = int(row['num_rings'])
try:
assert geom.geometry().numInteriorRings() == exp_geometries, "Ring count {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp_geometries, geom.geometry().numInteriorRings())
except:
#some geometry types don't have numInteriorRings()
assert exp_geometries <= 1, "Ring count {}: Expected:\n{} rings but could not call numInteriorRings()\n".format(i + 1, exp_geometries)

#test geometry centroid
exp = row['centroid']
result = geom.centroid().exportToWkt()
assert compareWkt(result, exp), "Centroid {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)

#test bounding box limits
bbox = geom.geometry().boundingBox()
exp = float(row['x_min'])
result = bbox.xMinimum()
assert doubleNear(result, exp), "Min X {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
exp = float(row['y_min'])
result = bbox.yMinimum()
assert doubleNear(result, exp), "Min Y {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
exp = float(row['x_max'])
result = bbox.xMaximum()
assert doubleNear(result, exp), "Max X {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
exp = float(row['y_max'])
result = bbox.yMaximum()
assert doubleNear(result, exp), "Max Y {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)

#test area calculation
exp = float(row['area'])
result = geom.geometry().area()
assert doubleNear(result, exp), "Area {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)

#NOTE - disabled due to misleading length/perimeter calculations for geometry collections
#exp = float(row['length'])
#result = geom.geometry().length()
#assert doubleNear(result, exp), "Length {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)
#exp = float(row['perimeter'])
#result = geom.geometry().length()
#assert doubleNear(result, exp), "Length {}: mismatch Expected:\n{}\nGot:\n{}\n".format(i + 1, exp, result)

def testArea(self):
""" Test area calculations """
with open(os.path.join(TEST_DATA_DIR, 'area_data.csv'), 'r') as d:
@@ -205,13 +205,43 @@ def writeShape(theMemoryLayer, theFileName):
assert myResult == QgsVectorFileWriter.NoError


def doubleNear(a, b, tol=0.0000000001):
"""
Tests whether two floats are near, within a specified tolerance
"""
return abs(float(a) - float(b)) < tol


def compareWkt(a, b, tol=0.000001):

This comment has been minimized.

Copy link
@m-kuhn

m-kuhn Jan 7, 2016

Member

Is it any better to use this method instead of QgsGeometry.compare()? It also takes an epsilon and does not require geometries to be converted to wkt.

This comment has been minimized.

Copy link
@nyalldawson

nyalldawson Jan 7, 2016

Author Collaborator

Depends on the context. I use this function a lot for comparing wkt output vs expected. But it would be better to use the geometry compare if that's what you're testing.

This comment has been minimized.

Copy link
@m-kuhn

m-kuhn Jan 7, 2016

Member

Further up in this diff the geometry is explicitly converted to wkt.

785d995#diff-fe3aa1328ee04f0eb00a1b1d59c0ea71R161

I think it would be great to have a method for it with some fancy gimmicks to visualize the differences.

This comment has been minimized.

Copy link
@nyalldawson

nyalldawson Jan 7, 2016

Author Collaborator

Ok, you're right - that test could be replaced by the compare method.

r0 = re.compile("-?\d+(?:\.\d+)?(?:[eE]\d+)?")
r1 = re.compile("\s*,\s*")
"""
Compares two WKT strings, ignoring allowed differences between strings
and allowing a tolerance for coordinates
"""
#ignore case
a0 = a.lower()
b0 = b.lower()

#remove optional spaces before z/m
r = re.compile("\s+([zm])")
a0 = r.sub(r'\1', a0)
b0 = r.sub(r'\1', b0)

#ignore the z/m flag on GeometryCollections
#NOTE - I'm not sure about this, possibly the flag is required and there's a bug in QGIS omitting this
r = re.compile("geometrycollection\s*[zm]*")
a0 = r.sub('geometrycollection', a0)
b0 = r.sub('geometrycollection', b0)

#spaces before brackets are optional
r = re.compile("\s*\(")
a0 = r.sub('(', a0)
b0 = r.sub('(', b0)

# compare the structure
a0 = r1.sub(",", r0.sub("#", a))
b0 = r1.sub(",", r0.sub("#", b))
r0 = re.compile("-?\d+(?:\.\d+)?(?:[eE]\d+)?")
r1 = re.compile("\s*,\s*")
a0 = r1.sub(",", r0.sub("#", a0))
b0 = r1.sub(",", r0.sub("#", b0))
if a0 != b0:
return False

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

for (a1, b1) in izip(a0, b0):
if abs(float(a1) - float(b1)) > tol:
if not doubleNear(a1, b1, tol):
return False

return True

0 comments on commit 785d995

Please sign in to comment.
You can’t perform that action at this time.