Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions owslib/feature/postrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,12 @@ def set_filter(self, filter):

Cannot be used with set_bbox() or set_featureid().
"""
f = etree.fromstring(filter)
sub_elem = f.find(util.nspath("Filter", FES_NAMESPACE))
if isinstance(filter, str):
f = etree.fromstring(filter)
sub_elem = f.find(util.nspath("Filter", FES_NAMESPACE))
else:
sub_elem = filter

self._query.append(sub_elem)

def set_maxfeatures(self, maxfeatures):
Expand Down
59 changes: 59 additions & 0 deletions owslib/fes2.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from owslib.etree import etree
from owslib import util
from owslib.namespaces import Namespaces
from abc import ABCMeta, abstractmethod


# default variables
Expand Down Expand Up @@ -391,6 +392,64 @@ def toXML(self):
return tmp


class Filter(OgcExpression):
def __init__(self, filter):
self.filter = filter

def toXML(self):
node = etree.Element(util.nspath_eval("fes:Filter", namespaces))
node.append(self.filter.toXML())
return node


class TopologicalOpType(OgcExpression, metaclass=ABCMeta):
"""Abstract base class for topological operators."""
@property
@abstractmethod
def operation(self):
"""This is a mechanism to ensure this class is subclassed by an actual operation."""
pass

def __init__(self, propertyname, geometry):
self.propertyname = propertyname
self.geometry = geometry

def toXML(self):
node = etree.Element(util.nspath_eval(f"fes:{self.operation}", namespaces))
etree.SubElement(node, util.nspath_eval("fes:ValueReference", namespaces)).text = self.propertyname
node.append(self.geometry.toXML())

return node


class Intersects(TopologicalOpType):
operation = "Intersects"


class Contains(TopologicalOpType):
operation = "Contains"


class Disjoint(TopologicalOpType):
operation = "Disjoint"


class Within(TopologicalOpType):
operation = "Within"


class Touches(TopologicalOpType):
operation = "Touches"


class Overlaps(TopologicalOpType):
operation = "Overlaps"


class Equals(TopologicalOpType):
operation = "Equals"


# BINARY
class BinaryLogicOpType(OgcExpression):
""" Binary Operators: And / Or """
Expand Down
69 changes: 69 additions & 0 deletions owslib/gml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from dataclasses import dataclass
from typing import Sequence
from owslib.etree import etree
from owslib import util
from owslib.namespaces import Namespaces


def get_namespaces():
n = Namespaces()
ns = n.get_namespaces(["gml32", "ogc", "xsd"])
ns[None] = n.get_namespace("ogc")
return ns


namespaces = get_namespaces()


def prefix(x):
"""Shorthand to insert namespaces."""
return util.nspath_eval(x, namespaces)


@dataclass
class AbstractGMLType():
id: str


@dataclass
class AbstractGeometryType(AbstractGMLType):
srsName: str = None
srsDimension: int = None
# axisLabels: str = None
# uomLabels: str = None

description: str = None
descriptionReference: str = None
identifier: str = None
name: str = None


@dataclass
class _PointBase:
"""This is used to avoid issues arising from non-optional attributes defined after optional attributes."""
pos: Sequence[float]


@dataclass
class Point(AbstractGeometryType, _PointBase):
"""GML Point object."""

def toXML(self):
"""Return `lxml.etree.Element` object."""
node = etree.Element(prefix("gml32:Point"))
for key in ["id", "srsName"]:
if getattr(self, key, None) is not None:
node.set(prefix(f"gml32:{key}"), getattr(self, key))

for key in ["description", "descriptionReference", "identifier", "name"]:
content = getattr(self, key)
if content is not None:
etree.SubElement(node, prefix(f"gml32:{key}")).text = content

coords = etree.SubElement(node, prefix("gml32:pos"))
coords.text = " ".join([str(c) for c in self.pos])
for key in ["srsDimension"]:
if getattr(self, key, None) is not None:
node.set(prefix(f"gml32:{key}"), getattr(self, key))

return node
3 changes: 2 additions & 1 deletion owslib/iso.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,8 @@ def __init__(self, md=None, identtype=None):
self.status = _testCodeListValue(md.find(util.nspath_eval('gmd:status/gmd:MD_ProgressCode', namespaces)))

self.graphicoverview = []
for val in md.findall(util.nspath_eval('gmd:graphicOverview/gmd:MD_BrowseGraphic/gmd:fileName/gco:CharacterString', namespaces)):
for val in md.findall(util.nspath_eval(
'gmd:graphicOverview/gmd:MD_BrowseGraphic/gmd:fileName/gco:CharacterString', namespaces)):
if val is not None:
val2 = util.testXMLValue(val)
if val2 is not None:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pytz
requests>=1.0
pyproj >=2
pyyaml
dataclasses; python_version < '3.7'
46 changes: 46 additions & 0 deletions tests/test_fes2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pytest
from owslib.wfs import WebFeatureService
from owslib import fes2
from owslib.gml import Point
from owslib.namespaces import Namespaces
from owslib import util
import json

n = Namespaces()
FES_NAMESPACE = n.get_namespace("fes")
GML32_NAMESPACE = n.get_namespace("gml32")


SERVICE_URL = "http://soggy.zoology.ubc.ca:8080/geoserver/wfs"


def test_raw_filter():
"""Just inspect the filter object (not embedded in a getfeature request)."""
point = Point(id="qc", srsName="http://www.opengis.net/gml/srs/epsg.xml#4326", pos=[-71, 46])
f = fes2.Filter(
fes2.And([fes2.Intersects(propertyname="the_geom", geometry=point),
fes2.PropertyIsLike("name", "value")]
)
)

xml = f.toXML()

# Fairly basic test
xml.find(util.nspath("Filter", FES_NAMESPACE))
xml.find(util.nspath("And", FES_NAMESPACE))
xml.find(util.nspath("Intersects", FES_NAMESPACE))
xml.find(util.nspath("Point", GML32_NAMESPACE))


@pytest.mark.online
def test_filter():
"""A request without filtering will yield 600 entries. With filtering we expect only one.

Note that this type of topological filtering only works (so far) with WFS 2.0.0 and POST requests.
"""
wfs = WebFeatureService(SERVICE_URL, version="2.0.0")
layer = "bcmca:commercialfish_crab"
point = Point(id="random", srsName="http://www.opengis.net/gml/srs/epsg.xml#4326", pos=[-129.8, 55.44])
f = fes2.Filter(fes2.Contains(propertyname="geom", geometry=point))
r = wfs.getfeature(layer, outputFormat="application/json", method="POST", filter=f.toXML())
assert json.load(r)["totalFeatures"] == 1
9 changes: 1 addition & 8 deletions tests/test_wfs_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from owslib.util import ServiceException
from urllib.parse import urlparse
from tests.utils import resource_file, sorted_url_query, service_ok

import json
import pytest

SERVICE_URL = 'https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288'
Expand Down Expand Up @@ -54,7 +54,6 @@ def test_getfeature():
@pytest.mark.skipif(not service_ok(SERVICE_URL),
reason="WFS service is unreachable")
def test_outputformat_wfs_100():
import json
wfs = WebFeatureService('https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288',
version='1.0.0')
feature = wfs.getfeature(
Expand All @@ -66,7 +65,6 @@ def test_outputformat_wfs_100():
@pytest.mark.skipif(not service_ok(SERVICE_URL),
reason="WFS service is unreachable")
def test_outputformat_wfs_110():
import json
wfs = WebFeatureService('https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288',
version='1.1.0')
feature = wfs.getfeature(
Expand All @@ -78,7 +76,6 @@ def test_outputformat_wfs_110():
@pytest.mark.skipif(not service_ok(SERVICE_URL),
reason="WFS service is unreachable")
def test_outputformat_wfs_200():
import json
wfs = WebFeatureService('https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288',
version='2.0.0')
feature = wfs.getfeature(
Expand All @@ -90,7 +87,6 @@ def test_outputformat_wfs_200():
@pytest.mark.skipif(not service_ok(SERVICE_URL),
reason="WFS service is unreachable")
def test_srsname_wfs_100():
import json
wfs = WebFeatureService('https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288',
version='1.0.0')
# ServiceException: Unable to support srsName: EPSG:99999999
Expand All @@ -99,7 +95,6 @@ def test_srsname_wfs_100():
typename=['sb:Project_Area'], maxfeatures=1, propertyname=None, outputFormat='application/json',
srsname="EPSG:99999999")

import json
wfs = WebFeatureService('https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288',
version='1.0.0')
feature = wfs.getfeature(
Expand All @@ -112,7 +107,6 @@ def test_srsname_wfs_100():
@pytest.mark.skipif(not service_ok(SERVICE_URL),
reason="WFS service is unreachable")
def test_srsname_wfs_110():
import json
wfs = WebFeatureService(
'https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288',
version='1.1.0')
Expand All @@ -122,7 +116,6 @@ def test_srsname_wfs_110():
typename=['sb:Project_Area'], maxfeatures=1, propertyname=None, outputFormat='application/json',
srsname="EPSG:99999999")

import json
wfs = WebFeatureService(
'https://www.sciencebase.gov/catalogMaps/mapping/ows/53398e51e4b0db25ad10d288',
version='1.0.0')
Expand Down