Skip to content

Commit

Permalink
Add some extra unit tests for geometry:
Browse files Browse the repository at this point in the history
- Add some tests for conversion to/from WKT, using a bulk lot of testsdata
from PostGIS
- Add some tests for area/length calculation, using some test data and
results from PostGIS/geos unit tests
- Add tests for spatial relations, using test data from PostGIS. Note
that this required adding support for calculating the DE-9IM relation. I'll
expose this to users via the expression engine in 2.14.

Along the way this also fixes a number of bugs relating to WKT geometry
import, such as
- add support for alternate MultiPoint(1 1,2 2,...) format
- fix GeometryCollection to support collections with multi* children
and GeometryCollection children (allowed by spec)
  • Loading branch information
nyalldawson committed Oct 16, 2015
1 parent 7f325d5 commit 55c27ce
Show file tree
Hide file tree
Showing 11 changed files with 787 additions and 31 deletions.
10 changes: 10 additions & 0 deletions python/core/geometry/qgsgeometryengine.sip
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ class QgsGeometryEngine
virtual bool overlaps( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;
virtual bool contains( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;
virtual bool disjoint( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;

/** Returns the Dimensional Extended 9 Intersection Model (DE-9IM) representation of the
* relationship between the geometries.
* @param geom geometry to relate to
* @param errorMsg destination storage for any error message
* @returns DE-9IM string for relationship, or an empty string if an error occurred
* @note added in QGIS 2.12
*/
virtual QString relate( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;

virtual double area( QString* errorMsg = 0 ) const = 0;
virtual double length( QString* errorMsg = 0 ) const = 0;
virtual bool isValid( QString* errorMsg = 0 ) const = 0;
Expand Down
9 changes: 7 additions & 2 deletions src/core/geometry/qgsgeometrycollectionv2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ email : marco.hugentobler at sourcepole dot com
#include "qgscircularstringv2.h"
#include "qgscompoundcurvev2.h"
#include "qgslinestringv2.h"
#include "qgsmultilinestringv2.h"
#include "qgspointv2.h"
#include "qgsmultipointv2.h"
#include "qgspolygonv2.h"
#include "qgsmultipolygonv2.h"
#include "qgswkbptr.h"

QgsGeometryCollectionV2::QgsGeometryCollectionV2(): QgsAbstractGeometryV2()
Expand Down Expand Up @@ -214,8 +217,10 @@ bool QgsGeometryCollectionV2::fromWkb( const unsigned char * wkb )

bool QgsGeometryCollectionV2::fromWkt( const QString& wkt )
{
return fromCollectionWkt( wkt, QList<QgsAbstractGeometryV2*>() << new QgsPointV2 << new QgsLineStringV2
<< new QgsCircularStringV2 << new QgsCompoundCurveV2, "GeometryCollection" );
return fromCollectionWkt( wkt, QList<QgsAbstractGeometryV2*>() << new QgsPointV2 << new QgsLineStringV2 << new QgsPolygonV2
<< new QgsCircularStringV2 << new QgsCompoundCurveV2
<< new QgsMultiPointV2 << new QgsMultiLineStringV2
<< new QgsMultiPolygonV2 << new QgsGeometryCollectionV2, "GeometryCollection" );
}

int QgsGeometryCollectionV2::wkbSize() const
Expand Down
10 changes: 10 additions & 0 deletions src/core/geometry/qgsgeometryengine.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ class CORE_EXPORT QgsGeometryEngine
virtual bool overlaps( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;
virtual bool contains( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;
virtual bool disjoint( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;

/** Returns the Dimensional Extended 9 Intersection Model (DE-9IM) representation of the
* relationship between the geometries.
* @param geom geometry to relate to
* @param errorMsg destination storage for any error message
* @returns DE-9IM string for relationship, or an empty string if an error occurred
* @note added in QGIS 2.12
*/
virtual QString relate( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const = 0;

virtual double area( QString* errorMsg = 0 ) const = 0;
virtual double length( QString* errorMsg = 0 ) const = 0;
virtual bool isValid( QString* errorMsg = 0 ) const = 0;
Expand Down
34 changes: 34 additions & 0 deletions src/core/geometry/qgsgeos.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,40 @@ bool QgsGeos::disjoint( const QgsAbstractGeometryV2& geom, QString* errorMsg ) c
return relation( geom, DISJOINT, errorMsg );
}

QString QgsGeos::relate( const QgsAbstractGeometryV2& geom, QString* errorMsg ) const
{
if ( !mGeos )
{
return QString();
}

GEOSGeomScopedPtr geosGeom( asGeos( &geom, mPrecision ) );
if ( !geosGeom )
{
return QString();
}

QString result;
try
{
char* r = GEOSRelate_r( geosinit.ctxt, mGeos, geosGeom.get() );
if ( r )
{
result = QString( r );
GEOSFree_r( geosinit.ctxt, r );
}
}
catch ( GEOSException &e )
{
if ( errorMsg )
{
*errorMsg = e.what();
}
}

return result;
}

double QgsGeos::area( QString* errorMsg ) const
{
double area = -1.0;
Expand Down
1 change: 1 addition & 0 deletions src/core/geometry/qgsgeos.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class CORE_EXPORT QgsGeos: public QgsGeometryEngine
bool overlaps( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const override;
bool contains( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const override;
bool disjoint( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const override;
QString relate( const QgsAbstractGeometryV2& geom, QString* errorMsg = 0 ) const override;
double area( QString* errorMsg = 0 ) const override;
double length( QString* errorMsg = 0 ) const override;
bool isValid( QString* errorMsg = 0 ) const override;
Expand Down
12 changes: 11 additions & 1 deletion src/core/geometry/qgsmultipointv2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ QgsMultiPointV2 *QgsMultiPointV2::clone() const

bool QgsMultiPointV2::fromWkt( const QString& wkt )
{
return fromCollectionWkt( wkt, QList<QgsAbstractGeometryV2*>() << new QgsPointV2, "Point" );
QString collectionWkt( wkt );
//test for non-standard MultiPoint(x1 y1, x2 y2) format
QRegExp regex( "^\\s*MultiPoint\\s*[ZM]*\\s*\\(\\s*\\d" );
regex.setCaseSensitivity( Qt::CaseInsensitive );
if ( regex.indexIn( collectionWkt ) >= 0 )
{
//alternate style without extra brackets, upgrade to standard
collectionWkt.replace( "(", "((" ).replace( ")", "))" ).replace( ",", "),(" );
}

return fromCollectionWkt( collectionWkt, QList<QgsAbstractGeometryV2*>() << new QgsPointV2, "Point" );
}

QDomElement QgsMultiPointV2::asGML2( QDomDocument& doc, int precision, const QString& ns ) const
Expand Down
106 changes: 78 additions & 28 deletions tests/src/python/test_qgsgeometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
# Convenience instances in case you may need them not used in this test

QGISAPP, CANVAS, IFACE, PARENT = getQgisTestApp()
TEST_DATA_DIR = unitTestDataPath()


class TestQgsGeometry(TestCase):
Expand All @@ -46,6 +47,27 @@ def testWktPointLoading(self):
(QGis.WKBPoint, myGeometry.type()))
assert myGeometry.wkbType() == QGis.WKBPoint, myMessage

def testWktMultiPointLoading(self):
#Standard format
wkt = 'MultiPoint ((10 15),(20 30))'
geom = QgsGeometry.fromWkt(wkt)
assert geom.wkbType() == QGis.WKBMultiPoint, ('Expected:\n%s\nGot:\n%s\n' % (QGis.WKBPoint, geom.type()))
assert geom.geometry().numGeometries() == 2
assert geom.geometry().geometryN(0).x() == 10
assert geom.geometry().geometryN(0).y() == 15
assert geom.geometry().geometryN(1).x() == 20
assert geom.geometry().geometryN(1).y() == 30

#Check MS SQL format
wkt = 'MultiPoint (11 16, 21 31)'
geom = QgsGeometry.fromWkt(wkt)
assert geom.wkbType() == QGis.WKBMultiPoint, ('Expected:\n%s\nGot:\n%s\n' % (QGis.WKBPoint, geom.type()))
assert geom.geometry().numGeometries() == 2
assert geom.geometry().geometryN(0).x() == 11
assert geom.geometry().geometryN(0).y() == 16
assert geom.geometry().geometryN(1).x() == 21
assert geom.geometry().geometryN(1).y() == 31

def testFromPoint(self):
myPoint = QgsGeometry.fromPoint(QgsPoint(10, 10))
myMessage = ('Expected:\n%s\nGot:\n%s\n' %
Expand Down Expand Up @@ -95,34 +117,48 @@ def testFromMultiPolygon(self):
assert myMultiPolygon.wkbType() == QGis.WKBMultiPolygon, myMessage

def testExportToWkt(self):

# test exporting collections to wkt. MultiPolygon, MultiLineString and MultiPoint should omit child types
wkt = "MultiPolygon (((0 0, 1 0, 1 1, 2 1, 2 2, 0 2, 0 0)),((4 0, 5 0, 5 2, 3 2, 3 1, 4 1, 4 0)))"
g = QgsGeometry.fromWkt(wkt)
res = g.exportToWkt()
assert compareWkt(res, wkt), "Expected:\n%s\nGot:\n%s\n" % (wkt, res)

wkt = "MultiLineString ((0 0, 1 0, 1 1, 2 1, 2 0), (3 1, 5 1, 5 0, 6 0))"
g = QgsGeometry.fromWkt(wkt)
res = g.exportToWkt()
assert compareWkt(res, wkt), "Expected:\n%s\nGot:\n%s\n" % (wkt, res)

wkt = "MultiPoint ((10 30),(40 20),(30 10),(20 10))"
g = QgsGeometry.fromWkt(wkt)
res = g.exportToWkt()
assert compareWkt(res, wkt), "Expected:\n%s\nGot:\n%s\n" % (wkt, res)

#mixed GeometryCollection should keep child types
wkt = "GeometryCollection (Point (10 10),Point (30 30),LineString (15 15, 20 20))"
g = QgsGeometry.fromWkt(wkt)
res = g.exportToWkt()
assert compareWkt(res, wkt), "Expected:\n%s\nGot:\n%s\n" % (wkt, res)

#Multicurve should keep child type
wkt = "MultiCurve (CircularString (90 232, 95 230, 100 232),CircularString (90 232, 95 234, 100 232))"
g = QgsGeometry.fromWkt(wkt)
res = g.exportToWkt()
assert compareWkt(res, wkt), "Expected:\n%s\nGot:\n%s\n" % (wkt, res)
""" Test parsing a whole range of valid wkt formats and variants.
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)
result = geom.exportToWkt()
if len(test_data) > 1:
exp = test_data[1]
else:
exp = test_data[0]
assert compareWkt(result, exp), "WKT conversion {}: 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:
for i, t in enumerate(d):
if not t:
continue

test_data = t.strip().split('|')
geom = QgsGeometry.fromWkt(test_data[0])
assert geom, "Area {} failed: could not create geom:\n{}\n".format(i + 1, test_data[0])
result = geom.area()
exp = float(test_data[1])
assert abs(float(result) - float(exp)) < 0.0000001, "Area failed: mismatch Expected:\n{}\nGot:\n{}\nGeom:\n{}\n".format(i + 1, exp, result, test_data[0])

def testLength(self):
""" Test length/perimeter calculations """
with open(os.path.join(TEST_DATA_DIR, 'length_data.csv'), 'r') as d:
for i, t in enumerate(d):
if not t:
continue

test_data = t.strip().split('|')
geom = QgsGeometry.fromWkt(test_data[0])
assert geom, "Length {} failed: could not create geom:\n{}\n".format(i + 1, test_data[0])
result = geom.length()
exp = float(test_data[1])
assert abs(float(result) - float(exp)) < 0.0000001, "Length {} failed: mismatch Expected:\n{}\nGot:\n{}\nGeom:\n{}\n".format(i + 1, exp, result, test_data[0])

def testIntersection(self):
myLine = QgsGeometry.fromPolyline([
Expand Down Expand Up @@ -1620,6 +1656,19 @@ def testAddMValue(self):
wkt = geom.exportToWkt()
assert compareWkt(expWkt, wkt), "addMValue to Point failed: mismatch Expected:\n%s\nGot:\n%s\n" % (expWkt, wkt)

def testRelates(self):
""" Test relationships between geometries. Note the bulk of these tests were taken from the PostGIS relate testdata """
with open(os.path.join(TEST_DATA_DIR, 'relates_data.csv'), 'r') as d:
for i, t in enumerate(d):
test_data = t.strip().split('|')
geom1 = QgsGeometry.fromWkt(test_data[0])
assert geom1, "Relates {} failed: could not create geom:\n{}\n".format(i + 1, test_data[0])
geom2 = QgsGeometry.fromWkt(test_data[1])
assert geom2, "Relates {} failed: could not create geom:\n{}\n".format(i + 1, test_data[1])
result = QgsGeometry.createGeometryEngine(geom1.geometry()).relate(geom2.geometry())
exp = test_data[2]
assert result == exp, "Relates {} failed: mismatch Expected:\n{}\nGot:\n{}\nGeom1:\n{}\nGeom2:\n{}\n".format(i + 1, exp, result, test_data[0], test_data[1])

def testWkbTypes(self):
""" Test QgsWKBTypes methods """

Expand Down Expand Up @@ -1854,5 +1903,6 @@ def testWkbTypes(self):
assert QgsWKBTypes.addM(QgsWKBTypes.MultiLineString25D) == QgsWKBTypes.MultiLineString25D
assert QgsWKBTypes.addM(QgsWKBTypes.MultiPolygon25D) == QgsWKBTypes.MultiPolygon25D


if __name__ == '__main__':
unittest.main()
13 changes: 13 additions & 0 deletions tests/testdata/area_data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
MultiPolygon( ((0 0, 10 0, 10 10, 0 10, 0 0)),( (0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7 , 5 7, 5 5) ) ,( (0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7, 5 7, 5 5),(1 1,2 1, 2 2, 1 2, 1 1) ) )|291
Point(1 2)|0
MultiPoint ((1 2),(2 3))|0
LineString(1 2,2 3,3 4)|0
Polygon (())|0
LineString ()|0
MultiPolygon ((()))|0
MultiLineString ()|0
MultiPoint ()|0
GeometryCollection ()|0
Polygon ((60 180, 140 240, 140 240, 140 240, 200 180, 120 120, 60 180))|8400
Polygon ((60 180, 140 120, 100 180, 140 240, 60 180))|2400
Polygon ((60 180, 140 120, 100 180, 140 240, 140 240, 60 180))|2400
11 changes: 11 additions & 0 deletions tests/testdata/length_data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
MultiPolygon( ((0 0, 10 0, 10 10, 0 10, 0 0)),( (0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7 , 5 7, 5 5) ) ,( (0 0, 10 0, 10 10, 0 10, 0 0),(5 5, 7 5, 7 7, 5 7, 5 5),(1 1,2 1, 2 2, 1 2, 1 1) ) )|140
MultiLineString((0 0, 1 1),(0 0, 1 1, 2 2) )|4.24264068711929
Point (1 2)|0
MultiPoint ((1 2),(2 3))|0
Polygon (())|0
LineString ()|0
MultiPolygon ((()))|0
MultiLineString ()|0
MultiPoint ()|0
GeometryCollection ()|0
LineString (0 0, 10 10, 20 0)|28.2842712
Loading

0 comments on commit 55c27ce

Please sign in to comment.