source: TI02-CSML/trunk/services/3rdParty/OWSLib-0.2.0/owslib/wfs.py @ 2194

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI02-CSML/trunk/services/3rdParty/OWSLib-0.2.0/owslib/wfs.py@2194
Revision 2194, 14.1 KB checked in by lawrence, 13 years ago (diff)

Adding various specs and 3rd party code of interest for the CSML
services development.

Line 
1# =============================================================================
2# OWSLib. Copyright (C) 2005 Sean C. Gillies
3#
4# Contact email: sgillies@frii.com
5#
6# $Id: wfs.py 503 2006-02-01 17:09:12Z dokai $
7# =============================================================================
8
9import cgi
10from cStringIO import StringIO
11from urllib import urlencode
12from urllib2 import urlopen
13
14from etree import etree
15
16WFS_NAMESPACE = 'http://www.opengis.net/wfs'
17OGC_NAMESPACE = 'http://www.opengis.net/ogc'
18
19def nspath(path, ns=WFS_NAMESPACE):
20    """
21    Prefix the given path with the given namespace identifier.
22   
23    Parameters
24    ----------
25    path : string
26        ElementTree API Compatible path expression
27
28    ns : string
29        The XML namespace. Defaults to WFS namespace.
30    """
31    components = []
32    for component in path.split("/"):
33        if component != '*':
34            component = "{%s}%s" % (ns, component)
35        components.append(component)
36    return "/".join(components)
37
38
39class ServiceException(Exception):
40    pass
41
42
43class WebFeatureService(object):
44    """Abstraction for OGC Web Feature Service (WFS).
45
46    Implements IWebFeatureService.
47    """
48   
49    def __init__(self, url, version='1.0.0', xml=None):
50        """Initialize."""
51        self.url = url
52        self.version = version
53        self._capabilities = None
54        if xml:
55            reader = WFSCapabilitiesReader(self.version)
56            self._capabilities = ServiceMetadata(reader.readString(xml))
57       
58    def _getcapproperty(self):
59        if not self._capabilities:
60            reader = WFSCapabilitiesReader(self.version)
61            self._capabilities = ServiceMetadata(reader.read(self.url))
62        return self._capabilities
63    capabilities = property(_getcapproperty, None)
64           
65    def getcapabilities(self):
66        """Request and return capabilities document from the WFS as a
67        file-like object."""
68        reader = WFSCapabilitiesReader(self.version)
69        return urlopen(reader.capabilities_url(self.url))
70   
71    def getfeature(self, typename=None, filter=None, bbox=None, featureid=None,
72                   featureversion=None, propertyname=['*'], maxfeatures=None,
73                   method='{http://www.opengis.net/wfs}Get'):
74        """Request and return feature data as a file-like object.
75       
76        Parameters
77        ----------
78        typename : list
79            List of typenames (string)
80        filter : string
81            XML-encoded OGC filter expression.
82        bbox : tuple
83            (left, bottom, right, top) in the feature type's coordinates.
84        featureid : list
85            List of unique feature ids (string)
86        featureversion : string
87            Default is most recent feature version.
88        propertyname : list
89            List of feature property names. '*' matches all.
90        maxfeatures : int
91            Maximum number of features to be returned.
92        method : string
93            Qualified name of the HTTP DCP method to use.
94
95        There are 3 different modes of use
96
97        1) typename and bbox (simple spatial query)
98        2) typename and filter (more expressive)
99        3) featureid (direct access to known features)
100        """
101        md = self.capabilities
102        base_url = md.getOperationByName('{http://www.opengis.net/wfs}GetFeature').methods[method]['url']
103        request = {'service': 'WFS', 'version': self.version, 'request': 'GetFeature'}
104       
105        # check featureid
106        if featureid:
107            request['featureid'] = ','.join(featureid)
108        elif bbox and typename:
109            request['bbox'] = ','.join([str(x) for x in bbox])
110        elif filter and typename:
111            request['filter'] = str(filter)
112        assert len(typename) > 0
113        request['typename'] = ','.join(typename)
114       
115        request['propertyname'] = ','.join(propertyname)
116        if featureversion: request['featureversion'] = str(featureversion)
117        if maxfeatures: request['maxfeatures'] = str(maxfeatures)
118       
119        data = urlencode(request)
120        if method == 'Post':
121            u = urlopen(base_url, data=data)
122        else:
123            u = urlopen(base_url + data)
124       
125        # check for service exceptions, rewrap, and return
126        # We're going to assume that anything with a content-length > 32k
127        # is data. We'll check anything smaller.
128        if int(u.info()['Content-Length']) < 32000:
129            data = u.read()
130            tree = etree.fromstring(data)
131            if tree.tag == "{%s}ServiceExceptionReport" % OGC_NAMESPACE:
132                se = tree.find(nspath('ServiceException', OGC_NAMESPACE))
133                raise ServiceException, str(se.text).strip()
134            else:
135                return StringIO(data)
136        else:
137            return u
138
139
140class ServiceMetadata(object):
141    """Abstraction for WFS metadata.
142   
143    Implements IServiceMetadata.
144    """
145
146    def __init__(self, infoset):
147        """Initialize from an element tree."""
148        self._root = infoset.getRoot()
149        # properties
150        self.service = self._root.find(nspath('Service/Name')).text
151        self.title = self._root.find(nspath('Service/Title')).text
152        self.abstract = self._root.find(nspath('Service/Abstract')).text
153        self.link = self._root.find(nspath('Service/OnlineResource')).text
154       
155        # operations []
156        self.operations = []
157        for elem in self._root.findall(nspath('Capability/Request/*')):
158            self.operations.append(OperationMetadata(elem))
159
160        # contents: our assumption is that services use a top-level layer
161        # as a metadata organizer, nothing more.
162        self.contents = []
163        top = self._root.find(nspath('FeatureTypeList'))
164        for elem in self._root.findall(nspath('FeatureTypeList/FeatureType')):
165            self.contents.append(ContentMetadata(elem, top))
166       
167        # keywords
168        self.keywords = []
169
170        self.provider = ContactMetadata(self._root.find('Service/ContactInformation'))
171
172    def getContentByName(self, name):
173        """Return a named content item."""
174        for item in self.contents:
175            if item.name == name:
176                return item
177        raise KeyError, "No content named %s" % name
178
179    def getOperationByName(self, name):
180        """Return a named content item."""
181        for item in self.operations:
182            if item.name == name:
183                return item
184        raise KeyError, "No operation named %s" % name
185
186
187class ContentMetadata:
188    """Abstraction for WMS metadata.
189   
190    Implements IMetadata.
191    """
192
193    def __init__(self, elem, parent):
194        """."""
195        self.name = elem.find(nspath('Name')).text
196        self.title = elem.find(nspath('Title')).text
197        # bboxes
198        self.boundingBox = None
199        b = elem.find(nspath('BoundingBox'))
200        if b is not None:
201            self.boundingBox = (float(b.attrib['minx']),float(b.attrib['miny']),
202                    float(b.attrib['maxx']), float(b.attrib['maxy']),
203                    b.attrib['SRS'])
204        self.boundingBoxWGS84 = None
205        b = elem.find(nspath('LatLongBoundingBox'))
206        if b is not None:
207            self.boundingBoxWGS84 = (
208                    float(b.attrib['minx']),float(b.attrib['miny']),
209                    float(b.attrib['maxx']), float(b.attrib['maxy']),
210                    )
211        # crs options
212        self.crsOptions = [srs.text for srs in elem.findall(nspath('SRS'))]
213
214        # verbs
215        self.verbOptions = [op.tag for op \
216            in parent.findall(nspath('Operations/*'))]
217        self.verbOptions + [op.tag for op \
218            in elem.findall(nspath('Operations/*')) \
219            if op.tag not in self.verbOptions]
220       
221
222class OperationMetadata:
223    """Abstraction for WMS metadata.
224   
225    Implements IMetadata.
226    """
227    def __init__(self, elem):
228        """."""
229        self.name = elem.tag
230        # formatOptions
231        self.formatOptions = [f.tag for f in elem.findall(nspath('ResultFormat/*'))]
232        methods = []
233        for verb in elem.findall(nspath('DCPType/HTTP/*')):
234            url = verb.attrib['onlineResource']
235            methods.append((verb.tag, {'url': url}))
236        self.methods = dict(methods)
237
238
239class ContactMetadata:
240    """Abstraction for contact details advertised in GetCapabilities.
241    """
242    # TODO: refactor with class from wms
243
244    def __init__(self, elem):
245        self.name = None
246        self.email = None
247
248        if elem:
249            self.name = elem.find('ContactPersonPrimary/ContactPerson').text
250            self.organization = elem.find('ContactPersonPrimary/ContactOrganization').text
251            address = elem.find('ContactAddress')
252            if address is not None:
253                try:   
254                    self.address = address.find('Address').text
255                    self.city = address.find('City').text
256                    self.region = address.find('StateOrProvince').text
257                    self.postcode = address.find('Postcode').text
258                    self.country = address.find('Country').text
259                except: pass
260            self.email = elem.find('ContactElectronicMailAddress').text
261
262
263class WFSCapabilitiesInfoset(object):
264    """High-level container for WFS Capabilities based on lxml.etree
265    """
266
267    def __init__(self, infoset):
268        """Initialize"""
269        self._infoset = infoset
270
271    #
272    # XML Node accessors
273    #
274   
275    def getRoot(self):
276        """
277        Returns the root node of the capabilities document.
278        """
279        return self._infoset
280
281    def getServiceNode(self):
282        """
283        Returns the <Service> node of the capabilities document.
284        """
285        return self.getRoot().find(nspath('Service'))
286
287    def getCapabilitiesNode(self):
288        """
289        Returns the <Capability> node of the capabilities document.
290        """
291        return self.getRoot().find(nspath('Capability'))
292
293    def getFeatureTypeNode(self):
294        """
295        Returns the <FeatureTypeList> node of the capabilities document.
296        """
297        return self.getRoot().find(nspath('FeatureTypeList'))
298
299    def getFilterNode(self):
300        """
301        Returns the <Filter_Capabilities> node of the capabilities document.
302        """
303        return self.getRoot().find(nspath('Filter_Capabilities'))
304
305    #
306    # Info accessors
307    #
308
309    def getServiceInfo(self):
310        """
311        Returns the WFS Service information packed in a dictionary.
312        """
313        service = self.getServiceNode()
314        info = {}
315        for tag in ('Name', 'Title', 'Abstract', 'Keyword', 'OnlineResource',
316                    'Fees', 'AccessConstraints'):
317            info[tag.lower()] = service.findtext(nspath(tag))
318        return info
319
320    def getCapabilityInfo(self):
321        """
322        Returns the WFS Capability information packed in a dictionary.
323        """
324        # Simplify the resource URLs, favoring GET over POST.
325        # Assume GML2.
326        capabilities = self.getCapabilitiesNode()
327        info = {}
328       
329        for key, path_id in [('capabilities', 'GetCapabilities'),
330                             ('description', 'DescribeFeatureType'),
331                             ('features', 'GetFeature')]:
332            get_path = nspath('Request/%s/DCPType/HTTP/Get' % path_id)
333            post_path = nspath('Request/%s/DCPType/HTTP/Post' % path_id)
334           
335            node = capabilities.find(get_path) or capabilities.find(post_path)
336            info[key] = node.get('onlineResource', None)
337        return info
338
339    def getFeatureTypeInfo(self):
340        """
341        Returns the WFS Feature type information as a list of dictionaries.
342        """
343        # Assume XMLSchema is used as the schema description language.
344        info = []
345        for featuretype in self.getFeatureTypeNode().getiterator(nspath('FeatureType')):
346            entry = {}
347            # Loop over simple text nodes
348            for tag in ('Name', 'Title', 'Abstract', 'SRS'):
349                entry[tag.lower()] = featuretype.findtext(nspath(tag))
350
351            # LatLongBoundingBox
352            entry['latlongboundingbox'] = []
353            for latlong in featuretype.findall(nspath('LatLongBoundingBox')):
354                entry['latlongboundingbox'].append('%s,%s,%s,%s' % (latlong.get('minx'),
355                                                                    latlong.get('miny'),
356                                                                    latlong.get('maxx'),
357                                                                    latlong.get('maxy')))
358           
359            # MetadataURL
360            entry['metadataurl'] = []
361            for metadataurl in featuretype.findall(nspath('MetadataURL')):
362                entry['metadataurl'].append(metadataurl.text)
363
364            info.append(entry)
365        return info
366
367
368class WFSCapabilitiesReader(object):
369    """Read and parse capabilities document into a lxml.etree infoset
370    """
371
372    def __init__(self, version='1.0'):
373        """Initialize"""
374        self.version = version
375        self._infoset = None
376
377    def capabilities_url(self, service_url):
378        """Return a capabilities url
379        """
380        qs = []
381        if service_url.find('?') != -1:
382            qs = cgi.parse_qsl(service_url.split('?')[1])
383
384        params = [x[0] for x in qs]
385
386        if 'service' not in params:
387            qs.append(('service', 'WFS'))
388        if 'request' not in params:
389            qs.append(('request', 'GetCapabilities'))
390        if 'version' not in params:
391            qs.append(('version', self.version))
392
393        urlqs = urlencode(tuple(qs))
394        return service_url.split('?')[0] + '?' + urlqs
395
396    def read(self, url):
397        """Get and parse a WFS capabilities document, returning an
398        instance of WFSCapabilitiesInfoset
399
400        Parameters
401        ----------
402        url : string
403            The URL to the WFS capabilities document.
404        """
405        request = self.capabilities_url(url)
406        u = urlopen(request)
407        return WFSCapabilitiesInfoset(etree.fromstring(u.read()))
408
409    def readString(self, st):
410        """Parse a WFS capabilities document, returning an
411        instance of WFSCapabilitiesInfoset
412
413        string should be an XML capabilities document
414        """
415        if not isinstance(st, str):
416            raise ValueError("String must be of type string, not %s" % type(st))
417        return WFSCapabilitiesInfoset(etree.fromstring(st))
418   
Note: See TracBrowser for help on using the repository browser.