2016-07-25 11:01:19 -04:00

441 lines
16 KiB
Python

# =============================================================================
# OWSLib. Copyright (C) 2005 Sean C. Gillies
#
# Contact email: sgillies@frii.com
#
# $Id: wfs.py 503 2006-02-01 17:09:12Z dokai $
# =============================================================================
from __future__ import (absolute_import, division, print_function)
import cgi
from six import PY2
from six.moves import cStringIO as StringIO
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from owslib.util import openURL, testXMLValue, extract_xml_list, ServiceException, xmltag_split
from owslib.etree import etree
from owslib.fgdc import Metadata
from owslib.iso import MD_Metadata
from owslib.crs import Crs
from owslib.namespaces import Namespaces
from owslib.util import log
from owslib.feature.schema import get_schema
import pyproj
n = Namespaces()
WFS_NAMESPACE = n.get_namespace("wfs")
OGC_NAMESPACE = n.get_namespace("ogc")
#TODO: use nspath in util.py
def nspath(path, ns=WFS_NAMESPACE):
"""
Prefix the given path with the given namespace identifier.
Parameters
----------
path : string
ElementTree API Compatible path expression
ns : string
The XML namespace. Defaults to WFS namespace.
"""
components = []
for component in path.split("/"):
if component != '*':
component = "{%s}%s" % (ns, component)
components.append(component)
return "/".join(components)
class WebFeatureService_1_0_0(object):
"""Abstraction for OGC Web Feature Service (WFS).
Implements IWebFeatureService.
"""
def __new__(self,url, version, xml, parse_remote_metadata=False, timeout=30):
""" overridden __new__ method
@type url: string
@param url: url of WFS capabilities document
@type xml: string
@param xml: elementtree object
@type parse_remote_metadata: boolean
@param parse_remote_metadata: whether to fully process MetadataURL elements
@param timeout: time (in seconds) after which requests should timeout
@return: initialized WebFeatureService_1_0_0 object
"""
obj=object.__new__(self)
obj.__init__(url, version, xml, parse_remote_metadata, timeout)
return obj
def __getitem__(self,name):
''' check contents dictionary to allow dict like access to service layers'''
if name in self.__getattribute__('contents').keys():
return self.__getattribute__('contents')[name]
else:
raise KeyError("No content named %s" % name)
def __init__(self, url, version, xml=None, parse_remote_metadata=False, timeout=30):
"""Initialize."""
self.url = url
self.version = version
self.timeout = timeout
self._capabilities = None
reader = WFSCapabilitiesReader(self.version)
if xml:
self._capabilities = reader.readString(xml)
else:
self._capabilities = reader.read(self.url)
self._buildMetadata(parse_remote_metadata)
def _buildMetadata(self, parse_remote_metadata=False):
'''set up capabilities metadata objects: '''
#serviceIdentification metadata
serviceelem=self._capabilities.find(nspath('Service'))
self.identification=ServiceIdentification(serviceelem, self.version)
#serviceProvider metadata
self.provider=ServiceProvider(serviceelem)
#serviceOperations metadata
self.operations=[]
for elem in self._capabilities.find(nspath('Capability/Request'))[:]:
self.operations.append(OperationMetadata(elem))
#serviceContents metadata: our assumption is that services use a top-level
#layer as a metadata organizer, nothing more.
self.contents={}
featuretypelist=self._capabilities.find(nspath('FeatureTypeList'))
features = self._capabilities.findall(nspath('FeatureTypeList/FeatureType'))
for feature in features:
cm=ContentMetadata(feature, featuretypelist, parse_remote_metadata)
self.contents[cm.id]=cm
#exceptions
self.exceptions = [f.text for f \
in self._capabilities.findall('Capability/Exception/Format')]
def getcapabilities(self):
"""Request and return capabilities document from the WFS as a
file-like object.
NOTE: this is effectively redundant now"""
reader = WFSCapabilitiesReader(self.version)
return openURL(reader.capabilities_url(self.url), timeout=self.timeout)
def items(self):
'''supports dict-like items() access'''
items=[]
for item in self.contents:
items.append((item,self.contents[item]))
return items
def _makeStringIO(self, strval):
"""
Helper method to make sure the StringIO being returned will work.
Differences between Python 2.6/2.7/3.x mean we have a lot of cases to handle.
"""
if PY2:
return StringIO(strval)
return StringIO(strval.decode())
def getfeature(self, typename=None, filter=None, bbox=None, featureid=None,
featureversion=None, propertyname='*', maxfeatures=None,
srsname=None, outputFormat=None, method='{http://www.opengis.net/wfs}Get',
startindex=None):
"""Request and return feature data as a file-like object.
Parameters
----------
typename : list
List of typenames (string)
filter : string
XML-encoded OGC filter expression.
bbox : tuple
(left, bottom, right, top) in the feature type's coordinates.
featureid : list
List of unique feature ids (string)
featureversion : string
Default is most recent feature version.
propertyname : list
List of feature property names. '*' matches all.
maxfeatures : int
Maximum number of features to be returned.
method : string
Qualified name of the HTTP DCP method to use.
srsname: string
EPSG code to request the data in
outputFormat: string (optional)
Requested response format of the request.
startindex: int (optional)
Start position to return feature set (paging in combination with maxfeatures)
There are 3 different modes of use
1) typename and bbox (simple spatial query)
2) typename and filter (more expressive)
3) featureid (direct access to known features)
"""
try:
base_url = next((m.get('url') for m in self.getOperationByName('GetFeature').methods if m.get('type').lower() == method.lower()))
except StopIteration:
base_url = self.url
request = {'service': 'WFS', 'version': self.version, 'request': 'GetFeature'}
# check featureid
if featureid:
request['featureid'] = ','.join(featureid)
elif bbox and typename:
request['bbox'] = ','.join([repr(x) for x in bbox])
elif filter and typename:
request['filter'] = str(filter)
if srsname:
request['srsname'] = str(srsname)
assert len(typename) > 0
request['typename'] = ','.join(typename)
if propertyname is not None:
if not isinstance(propertyname, list):
propertyname = [propertyname]
request['propertyname'] = ','.join(propertyname)
if featureversion: request['featureversion'] = str(featureversion)
if maxfeatures: request['maxfeatures'] = str(maxfeatures)
if startindex: request['startindex'] = str(startindex)
if outputFormat is not None:
request["outputFormat"] = outputFormat
data = urlencode(request)
log.debug("Making request: %s?%s" % (base_url, data))
u = openURL(base_url, data, method, timeout=self.timeout)
# check for service exceptions, rewrap, and return
# We're going to assume that anything with a content-length > 32k
# is data. We'll check anything smaller.
if 'Content-Length' in u.info():
length = int(u.info()['Content-Length'])
have_read = False
else:
data = u.read()
have_read = True
length = len(data)
if length < 32000:
if not have_read:
data = u.read()
try:
tree = etree.fromstring(data)
except BaseException:
# Not XML
return self._makeStringIO(data)
else:
if tree.tag == "{%s}ServiceExceptionReport" % OGC_NAMESPACE:
se = tree.find(nspath('ServiceException', OGC_NAMESPACE))
raise ServiceException(str(se.text).strip())
else:
return self._makeStringIO(data)
else:
if have_read:
return self._makeStringIO(data)
return u
def getOperationByName(self, name):
"""Return a named content item."""
for item in self.operations:
if item.name == name:
return item
raise KeyError("No operation named %s" % name)
def get_schema(self, typename):
"""
Get layer schema compatible with :class:`fiona` schema object
"""
return get_schema(self.url, typename, self.version)
class ServiceIdentification(object):
''' Implements IServiceIdentificationMetadata '''
def __init__(self, infoset, version):
self._root=infoset
self.type = testXMLValue(self._root.find(nspath('Name')))
self.version = version
self.title = testXMLValue(self._root.find(nspath('Title')))
self.abstract = testXMLValue(self._root.find(nspath('Abstract')))
self.keywords = [f.text for f in self._root.findall(nspath('Keywords'))]
self.fees = testXMLValue(self._root.find(nspath('Fees')))
self.accessconstraints = testXMLValue(self._root.find(nspath('AccessConstraints')))
class ServiceProvider(object):
''' Implements IServiceProviderMetatdata '''
def __init__(self, infoset):
self._root = infoset
self.name = testXMLValue(self._root.find(nspath('Name')))
self.url = testXMLValue(self._root.find(nspath('OnlineResource')))
self.keywords = extract_xml_list(self._root.find(nspath('Keywords')))
class ContentMetadata:
"""Abstraction for WFS metadata.
Implements IMetadata.
"""
def __init__(self, elem, parent, parse_remote_metadata=False, timeout=30):
"""."""
self.id = testXMLValue(elem.find(nspath('Name')))
self.title = testXMLValue(elem.find(nspath('Title')))
self.abstract = testXMLValue(elem.find(nspath('Abstract')))
self.keywords = [f.text for f in elem.findall(nspath('Keywords'))]
# bboxes
self.boundingBox = None
b = elem.find(nspath('LatLongBoundingBox'))
srs = elem.find(nspath('SRS'))
if b is not None:
self.boundingBox = (float(b.attrib['minx']),float(b.attrib['miny']),
float(b.attrib['maxx']), float(b.attrib['maxy']), Crs(srs.text))
# transform wgs84 bbox from given default bboxt
self.boundingBoxWGS84 = None
if b is not None and srs is not None:
wgs84 = pyproj.Proj(init="epsg:4326")
try:
src_srs = pyproj.Proj(init=srs.text)
mincorner = pyproj.transform(src_srs, wgs84, b.attrib['minx'],
b.attrib['miny'])
maxcorner = pyproj.transform(src_srs, wgs84, b.attrib['maxx'],
b.attrib['maxy'])
self.boundingBoxWGS84 = (mincorner[0], mincorner[1],
maxcorner[0], maxcorner[1])
except RuntimeError as e:
pass
# crs options
self.crsOptions = [Crs(srs.text) for srs in elem.findall(nspath('SRS'))]
# verbs
self.verbOptions = [op.tag for op \
in parent.findall(nspath('Operations/*'))]
self.verbOptions + [op.tag for op \
in elem.findall(nspath('Operations/*')) \
if op.tag not in self.verbOptions]
#others not used but needed for iContentMetadata harmonisation
self.styles=None
self.timepositions=None
self.defaulttimeposition=None
# MetadataURLs
self.metadataUrls = []
for m in elem.findall(nspath('MetadataURL')):
metadataUrl = {
'type': testXMLValue(m.attrib['type'], attrib=True),
'format': testXMLValue(m.find('Format')),
'url': testXMLValue(m)
}
if metadataUrl['url'] is not None and parse_remote_metadata: # download URL
try:
content = openURL(metadataUrl['url'], timeout=timeout)
doc = etree.parse(content)
if metadataUrl['type'] is not None:
if metadataUrl['type'] == 'FGDC':
metadataUrl['metadata'] = Metadata(doc)
if metadataUrl['type'] == 'TC211':
metadataUrl['metadata'] = MD_Metadata(doc)
except Exception:
metadataUrl['metadata'] = None
self.metadataUrls.append(metadataUrl)
class OperationMetadata:
"""Abstraction for WFS metadata.
Implements IMetadata.
"""
def __init__(self, elem):
"""."""
self.name = xmltag_split(elem.tag)
# formatOptions
self.formatOptions = [f.tag for f in elem.findall(nspath('ResultFormat/*'))]
self.methods = []
for verb in elem.findall(nspath('DCPType/HTTP/*')):
url = verb.attrib['onlineResource']
self.methods.append({'type' : xmltag_split(verb.tag), 'url': url})
class WFSCapabilitiesReader(object):
"""Read and parse capabilities document into a lxml.etree infoset
"""
def __init__(self, version='1.0'):
"""Initialize"""
self.version = version
self._infoset = None
def capabilities_url(self, service_url):
"""Return a capabilities url
"""
qs = []
if service_url.find('?') != -1:
qs = cgi.parse_qsl(service_url.split('?')[1])
params = [x[0] for x in qs]
if 'service' not in params:
qs.append(('service', 'WFS'))
if 'request' not in params:
qs.append(('request', 'GetCapabilities'))
if 'version' not in params:
qs.append(('version', self.version))
urlqs = urlencode(tuple(qs))
return service_url.split('?')[0] + '?' + urlqs
def read(self, url, timeout=30):
"""Get and parse a WFS capabilities document, returning an
instance of WFSCapabilitiesInfoset
Parameters
----------
url : string
The URL to the WFS capabilities document.
timeout : number
A timeout value (in seconds) for the request.
"""
request = self.capabilities_url(url)
u = openURL(request, timeout=timeout)
return etree.fromstring(u.read())
def readString(self, st):
"""Parse a WFS capabilities document, returning an
instance of WFSCapabilitiesInfoset
string should be an XML capabilities document
"""
if not isinstance(st, str) and not isinstance(st, bytes):
raise ValueError("String must be of type string or bytes, not %s" % type(st))
return etree.fromstring(st)