Skip to content

Commit

Permalink
Merge pull request #246 from DOV-Vlaanderen/stable-wfs-queries
Browse files Browse the repository at this point in the history
Generate stable WFS GetFeature requests
  • Loading branch information
Roel authored May 29, 2020
2 parents 1e584b1 + b3d8e9a commit b0b0718
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 14 deletions.
10 changes: 6 additions & 4 deletions pydov/util/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
import os
from collections import OrderedDict

from owslib.etree import etree
from owslib.fes import (
Expand Down Expand Up @@ -507,7 +508,7 @@ def __init__(self, gml, location_filter, location_filter_kwargs=None,
"""
self.gml = gml
self.subelements = set()
self.subelements = OrderedDict()

if location_filter_kwargs is None:
location_filter_kwargs = {}
Expand Down Expand Up @@ -543,11 +544,12 @@ def _dedup_multi(self, tree, xpath_single, xpath_multi):
Sets of the parsed single and multi geometry Elements.
"""
single = set(tree.findall(xpath_single))
multi = set(tree.findall(xpath_multi))
single = OrderedDict.fromkeys(tree.findall(xpath_single))
multi = OrderedDict.fromkeys(tree.findall(xpath_multi))

for m in multi:
single -= set(m.findall(xpath_single))
s_in_m = OrderedDict.fromkeys(m.findall(xpath_single))
single = OrderedDict.fromkeys(e for e in single if e not in s_in_m)

return single, multi

Expand Down
2 changes: 1 addition & 1 deletion pydov/util/owsutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ def wfs_build_getfeature_request(typename, geometry_column=None, location=None,
query.set('typeName', typename)

if propertyname and len(propertyname) > 0:
for property in propertyname:
for property in sorted(propertyname):
propertyname_xml = etree.Element(
'{http://www.opengis.net/wfs}PropertyName')
propertyname_xml.text = property
Expand Down
2 changes: 1 addition & 1 deletion pydov/util/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __init__(self, propertyname, lst):
self.query = PropertyIsEqualTo(propertyname, set(lst).pop())
else:
self.query = Or(
[PropertyIsEqualTo(propertyname, i) for i in set(lst)])
[PropertyIsEqualTo(propertyname, i) for i in sorted(set(lst))])

def toXML(self):
"""Return the XML representation of the PropertyInList query.
Expand Down
224 changes: 224 additions & 0 deletions tests/test_util_location_gmlfilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,35 @@ def test_point_multiple_31370(self):

assert len(points) == 0

def test_point_multiple_31370_stable(self):
"""Test the WithinDistance filter with a GML containing multiple
point geometries in EPSG:31370.
Test whether the generated XML is correct and stable.
"""
with open('tests/data/util/location/point_multiple_31370.gml',
'r') as gml_file:
gml = gml_file.read()

for i in range(10):
f = GmlFilter(gml, WithinDistance, {'distance': 100})
f.set_geometry_column('geom')
xml = f.toXML()

assert clean_xml(etree.tostring(xml).decode('utf8')) == clean_xml(
'<ogc:Or><ogc:DWithin><ogc:PropertyName>geom</ogc'
':PropertyName><gml:Point '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:pos>109124'
'.660670233 194937.206683695</gml:pos></gml:Point><gml'
':Distance units="meter">100.000000</gml:Distance></ogc'
':DWithin><ogc:DWithin><ogc:PropertyName>geom</ogc'
':PropertyName><gml:Point '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:pos>109234'
'.969526552 195772.402310114</gml:pos></gml:Point><gml'
':Distance units="meter">100.000000</gml:Distance>'
'</ogc:DWithin></ogc:Or>')

def test_point_single_4326(self):
"""Test the WithinDistance filter with a GML containing a single
point geometry in EPSG:4326.
Expand Down Expand Up @@ -159,6 +188,43 @@ def test_multipoint_multiple_31370(self):

assert len(multipoints) == 0

def test_multipoint_multiple_31370_stable(self):
"""Test the WithinDistance filter with a GML containing multiple
multipoint geometries in EPSG:31370.
Test whether the generated XML is correct and stable.
"""
with open('tests/data/util/location/multipoint_multiple_31370.gml',
'r') as gml_file:
gml = gml_file.read()

for i in range(10):
f = GmlFilter(gml, WithinDistance, {'distance': 100})
f.set_geometry_column('geom')
xml = f.toXML()

assert clean_xml(etree.tostring(xml).decode('utf8')) == clean_xml(
'<ogc:Or><ogc:DWithin><ogc:PropertyName>geom</ogc'
':PropertyName><gml:MultiPoint '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:pointMember><gml'
':Point><gml:pos>108770.096489206 '
'194992.361111855</gml:pos></gml:Point></gml:pointMember'
'><gml:pointMember><gml:Point><gml:pos>109045.868630005 '
'194929.327479672</gml:pos></gml:Point></gml:pointMember'
'></gml:MultiPoint><gml:Distance '
'units="meter">100.000000</gml:Distance></ogc:DWithin><ogc'
':DWithin><ogc:PropertyName>geom</ogc:PropertyName><gml'
':MultiPoint '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:pointMember><gml'
':Point><gml:pos>108825.250917366 '
'195433.596537133</gml:pos></gml:Point></gml:pointMember'
'><gml:pointMember><gml:Point><gml:pos>108738.579673115 '
'195614.818229658</gml:pos></gml:Point></gml:pointMember'
'></gml:MultiPoint><gml:Distance '
'units="meter">100.000000</gml:Distance></ogc:DWithin>'
'</ogc:Or>')


class TestLine(object):
"""Class grouping tests for line locations."""
Expand Down Expand Up @@ -223,6 +289,39 @@ def test_line_multiple_31370(self):

assert len(lines) == 0

def test_line_multiple_31370_stable(self):
"""Test the WithinDistance filter with a GML containing multiple
line geometries in EPSG:31370.
Test whether the generated XML is correct and stable.
"""
with open('tests/data/util/location/line_multiple_31370.gml',
'r') as gml_file:
gml = gml_file.read()

for i in range(10):
f = GmlFilter(gml, WithinDistance, {'distance': 100})
f.set_geometry_column('geom')
xml = f.toXML()

assert clean_xml(etree.tostring(xml).decode('utf8')) == clean_xml(
'<ogc:Or><ogc:DWithin><ogc:PropertyName>geom</ogc'
':PropertyName><gml :LineString '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:posList>108344'
'.619471974 195008.119519901 108801.613305297 '
'194842.656235421 109077.385446096 '
'195094.790764152</gml:posList></gml:LineString><gml'
':Distance units="meter">100.000000</gml:Distance></ogc'
':DWithin><ogc:DWithin><ogc:PropertyName>geom</ogc'
':PropertyName><gml :LineString '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:posList>108194'
'.91459554 196032.416042867 108714.942061046 '
'195528.146985407 108911.922161617 '
'195528.146985407</gml:posList></gml:LineString><gml'
':Distance units="meter">100.000000</gml:Distance>'
'</ogc:DWithin></ogc:Or>')


class TestMultiline(object):
"""Class grouping tests for multiline locations."""
Expand Down Expand Up @@ -295,6 +394,49 @@ def test_multiline_multiple_31370(self):

assert len(multilines) == 0

def test_multiline_multiple_31370_stable(self):
"""Test the WithinDistance filter with a GML containing multiple
multiline geometries in EPSG:31370.
Test whether the generated XML is correct.
"""
with open('tests/data/util/location/multiline_multiple_31370.gml',
'r') as gml_file:
gml = gml_file.read()

for i in range(10):
f = GmlFilter(gml, WithinDistance, {'distance': 100})
f.set_geometry_column('geom')
xml = f.toXML()

assert clean_xml(etree.tostring(xml).decode('utf8')) == clean_xml(
'<ogc:Or><ogc:DWithin><ogc:PropertyName>geom</ogc'
':PropertyName><gml:MultiCurve '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:curveMember><gml'
':LineString><gml:posList>108210.673003586 194850.535439444 '
'108454.928328293 195031.757131969 108746.458877137 '
'194834.777031398</gml:posList></gml:LineString></gml'
':curveMember><gml:curveMember><gml:LineString><gml:posList'
'>109164.056690347 195055.394744037 109211.331914484 '
'194661.434542896 109416.191219077 '
'194440.816830258</gml:posList></gml:LineString></gml'
':curveMember></gml:MultiCurve><gml:Distance '
'units="meter">100.000000</gml:Distance></ogc:DWithin><ogc'
':DWithin><ogc:PropertyName>geom</ogc:PropertyName><gml'
':MultiCurve '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:curveMember><gml'
':LineString><gml:posList>108226.431411631 196095.44967505 '
'108384.015492088 196276.671367574 108580.995592658 '
'196048.174450913</gml:posList></gml:LineString></gml'
':curveMember><gml:curveMember><gml:LineString><gml:posList'
'>108911.922161617 196379.101019871 109030.110221959 '
'196552.443508373 109282.244750689 '
'196607.597936533</gml:posList></gml:LineString></gml'
':curveMember></gml:MultiCurve><gml:Distance '
'units="meter">100.000000</gml:Distance></ogc:DWithin>'
'</ogc:Or>')


class TestPolygon(object):
"""Class grouping tests for polygon locations."""
Expand Down Expand Up @@ -363,6 +505,39 @@ def test_polyon_multiple_31370(self):

assert len(polygons) == 0

def test_polyon_multiple_31370_stable(self):
"""Test the Within filter with a GML containing multiple
polygon geometries in EPSG:31370.
Test whether the generated XML is correct and stable.
"""
with open('tests/data/util/location/polygon_multiple_31370.gml',
'r') as gml_file:
gml = gml_file.read()

for i in range(10):
f = GmlFilter(gml, Within)
f.set_geometry_column('geom')
xml = f.toXML()

assert clean_xml(etree.tostring(xml).decode('utf8')) == clean_xml(
'<ogc:Or><ogc:Within><ogc:PropertyName>geom</ogc'
':PropertyName><gml:Polygon '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:exterior'
'><gml:LinearRing><gml:posList>108636.150020818 '
'194960.844295764 108911.922161617 194291.111953824 '
'109195.573506438 195118.42837622 108636.150020818 '
'194960.844295764</gml:posList></gml:LinearRing></gml'
':exterior></gml:Polygon></ogc:Within><ogc:Within><ogc'
':PropertyName>geom</ogc:PropertyName><gml:Polygon '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:exterior'
'><gml:LinearRing><gml:posList>107485.786233486 '
'196741.544404921 107840.350414513 196339.704999757 '
'108297.344247837 196843.974057217 107485.786233486 '
'196741.544404921</gml:posList></gml:LinearRing></gml'
':exterior></gml:Polygon></ogc:Within></ogc:Or>')

def test_polyon_multiple_disjoint_31370(self):
"""Test the Disjoint filter with the And combinator with a GML
containing multiple polygon geometries in EPSG:31370.
Expand Down Expand Up @@ -481,6 +656,55 @@ def test_multipolygon_multiple_31370(self):

assert len(multipolygons) == 0

def test_multipolygon_multiple_31370_stable(self):
"""Test the Within filter with a GML containing multiple
multipolygon geometries in EPSG:31370.
Test whether the generated XML is correct.
"""
with open('tests/data/util/location/multipolygon_multiple_31370.gml',
'r') as gml_file:
gml = gml_file.read()

for i in range(10):
f = GmlFilter(gml, Within)
f.set_geometry_column('geom')
xml = f.toXML()

assert clean_xml(etree.tostring(xml).decode('utf8')) == clean_xml(
'<ogc:Or><ogc:Within><ogc:PropertyName>geom</ogc'
':PropertyName><gml:MultiSurface '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:surfaceMember'
'><gml:Polygon><gml:exterior><gml:LinearRing><gml:posList'
'>107564.578273715 196646.993956647 107785.195986354 '
'196386.980223894 107966.417678878 197143.383810084 '
'107564.578273715 '
'196646.993956647</gml:posList></gml:LinearRing'
'></gml:exterior></gml:Polygon></gml:surfaceMember><gml'
':surfaceMember><gml:Polygon><gml:exterior><gml:LinearRing'
'><gml:posList>108384.015492088 197214.29664629 '
'108447.04912427 196489.40987619 108785.854897252 '
'197269.45107445 108384.015492088 '
'197214.29664629</gml:posList></gml:LinearRing'
'></gml:exterior></gml:Polygon></gml:surfaceMember></gml'
':MultiSurface></ogc:Within><ogc:Within><ogc:PropertyName'
'>geom</ogc:PropertyName><gml:MultiSurface '
'srsName="urn:ogc:def:crs:EPSG::31370"><gml:surfaceMember'
'><gml:Polygon><gml:exterior><gml:LinearRing><gml:posList'
'>108588.874796681 195015.998723923 108911.922161617 '
'194251.71593371 109195.573506438 195134.186784266 '
'108588.874796681 '
'195015.998723923</gml:posList></gml:LinearRing'
'></gml:exterior></gml:Polygon></gml:surfaceMember><gml'
':surfaceMember><gml:Polygon><gml:exterior><gml:LinearRing'
'><gml:posList>109140.419078278 195748.764698045 '
'109400.432811031 195307.529272768 109597.412911602 '
'195772.402310114 109140.419078278 '
'195748.764698045</gml:posList></gml:LinearRing'
'></gml:exterior></gml:Polygon></gml:surfaceMember></gml'
':MultiSurface></ogc:Within></ogc:Or>')


class TestCombination(object):
"""Class grouping tests for combinations of locations."""
Expand Down
33 changes: 25 additions & 8 deletions tests/test_util_owsutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,27 @@ def test_wfs_build_getfeature_request_propertyname(self):
'xsi:schemaLocation="http://www.opengis.net/wfs '
'http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query '
'typeName="dov-pub:Boringen"> '
'<wfs:PropertyName>fiche</wfs:PropertyName> '
'<wfs:PropertyName>diepte_tot_m</wfs:PropertyName> <ogc:Filter/> '
'<wfs:PropertyName>diepte_tot_m</wfs:PropertyName> '
'<wfs:PropertyName>fiche</wfs:PropertyName> <ogc:Filter/> '
'</wfs:Query> </wfs:GetFeature>')

def test_wfs_build_getfeature_request_propertyname_stable(self):
"""Test the owsutil.wfs_build_getfeature_request method with a list
of propertynames.
Test whether the XML of the WFS GetFeature that is being generated is
stable (i.e. independent of the order of the propertynames).
"""
xml = owsutil.wfs_build_getfeature_request(
'dov-pub:Boringen', propertyname=['fiche', 'diepte_tot_m'])

xml2 = owsutil.wfs_build_getfeature_request(
'dov-pub:Boringen', propertyname=['diepte_tot_m', 'fiche'])

assert clean_xml(etree.tostring(xml).decode('utf8')) == clean_xml(
etree.tostring(xml2).decode('utf8'))

def test_wfs_build_getfeature_request_filter(self):
"""Test the owsutil.wfs_build_getfeature_request method with an
attribute filter.
Expand Down Expand Up @@ -463,8 +480,8 @@ def test_wfs_build_getfeature_request_bbox_filter_propertyname(self):
'xsi:schemaLocation="http://www.opengis.net/wfs '
'http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"> <wfs:Query '
'typeName="dov-pub:Boringen"> '
'<wfs:PropertyName>fiche</wfs:PropertyName> '
'<wfs:PropertyName>diepte_tot_m</wfs:PropertyName> <ogc:Filter> '
'<wfs:PropertyName>diepte_tot_m</wfs:PropertyName> '
'<wfs:PropertyName>fiche</wfs:PropertyName> <ogc:Filter> '
'<ogc:And> <ogc:PropertyIsEqualTo> '
'<ogc:PropertyName>gemeente</ogc:PropertyName> '
'<ogc:Literal>Herstappe</ogc:Literal> </ogc:PropertyIsEqualTo> '
Expand Down Expand Up @@ -496,8 +513,8 @@ def test_wfs_build_getfeature_request_sortby(self):
'service="WFS" version="1.1.0" '
'xsi:schemaLocation="http://www.opengis.net/wfs '
'http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"><wfs:Query '
'typeName="dov-pub:Boringen"><wfs:PropertyName>fiche</wfs'
':PropertyName><wfs:PropertyName>diepte_tot_m</wfs:PropertyName'
'typeName="dov-pub:Boringen"><wfs:PropertyName>diepte_tot_m</wfs'
':PropertyName><wfs:PropertyName>fiche</wfs:PropertyName'
'><ogc:Filter/><ogc:SortBy><ogc:SortProperty><ogc:PropertyName'
'>diepte_tot_m</ogc:PropertyName><ogc:SortOrder>DESC</ogc'
':SortOrder></ogc:SortProperty></ogc:SortBy></wfs:Query></wfs'
Expand Down Expand Up @@ -525,8 +542,8 @@ def test_wfs_build_getfeature_request_sortby_multi(self):
'service="WFS" version="1.1.0" '
'xsi:schemaLocation="http://www.opengis.net/wfs '
'http://schemas.opengis.net/wfs/1.1.0/wfs.xsd"><wfs:Query '
'typeName="dov-pub:Boringen"><wfs:PropertyName>fiche</wfs'
':PropertyName><wfs:PropertyName>diepte_tot_m</wfs:PropertyName'
'typeName="dov-pub:Boringen"><wfs:PropertyName>diepte_tot_m</wfs'
':PropertyName><wfs:PropertyName>fiche</wfs:PropertyName'
'><ogc:Filter/><ogc:SortBy><ogc:SortProperty><ogc:PropertyName'
'>diepte_tot_m</ogc:PropertyName><ogc:SortOrder>DESC</ogc'
':SortOrder></ogc:SortProperty><ogc:SortProperty><ogc'
Expand Down
Loading

0 comments on commit b0b0718

Please sign in to comment.