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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI02-CSML/trunk/services/3rdParty/OWSLib-0.2.0/owslib/wms.py@2194
Revision 2194, 15.7 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# -*- coding: ISO-8859-15 -*-
2# =============================================================================
3# Copyright (c) 2004, 2006 Sean C. Gillies
4# Copyright (c) 2005 Nuxeo SARL <http://nuxeo.com>
5#
6# Authors : Sean Gillies <sgillies@frii.com>
7#           Julien Anguenot <ja@nuxeo.com>
8#
9# Contact email: sgillies@frii.com
10# =============================================================================
11
12"""
13API for Web Map Service (WMS) methods and metadata.
14
15Currently supports only version 1.1.1 of the WMS protocol.
16"""
17
18import cgi
19from urllib import urlencode
20from urllib2 import urlopen
21
22from etree import etree
23
24
25class ServiceException(Exception):
26    pass
27
28
29class CapabilitiesError(Exception):
30    pass
31
32
33class WebMapService(object):
34    """Abstraction for OGC Web Map Service (WMS).
35
36    Implements IWebMapService.
37    """
38   
39    def __init__(self, url, version='1.1.1', xml=None):
40        """Initialize."""
41        self.url = url
42        self.version = version
43        self._capabilities = None
44        # initialize from saved capability document
45        if xml:
46            reader = WMSCapabilitiesReader(self.version)
47            self._capabilities = ServiceMetadata(reader.readString(xml))
48       
49    def _getcapproperty(self):
50        if not self._capabilities:
51            reader = WMSCapabilitiesReader(self.version)
52            self._capabilities = ServiceMetadata(reader.read(self.url))
53        return self._capabilities
54    capabilities = property(_getcapproperty, None)
55           
56    def getcapabilities(self):
57        """Request and return capabilities document from the WMS as a
58        file-like object."""
59        reader = WMSCapabilitiesReader(self.version)
60        u = urlopen(reader.capabilities_url(self.url))
61        # check for service exceptions, and return
62        if u.info().gettype() == 'application/vnd.ogc.se_xml':
63            se_xml = u.read()
64            se_tree = etree.fromstring(se_xml)
65            raise ServiceException, \
66                str(se_tree.find('ServiceException').text).strip()
67        return u
68
69    def getmap(self, layers=None, styles=None, srs=None, bbox=None,
70               format=None, size=None, transparent=False, bgcolor='#FFFFFF',
71               exceptions='application/vnd.ogc.se_xml',
72               method='Get'):
73        """Request and return an image from the WMS as a file-like object.
74       
75        Parameters
76        ----------
77        layers : list
78            List of content layer names.
79        styles : list
80            Optional list of named styles, must be the same length as the
81            layers list.
82        srs : string
83            A spatial reference system identifier.
84        bbox : tuple
85            (left, bottom, right, top) in srs units.
86        format : string
87            Output image format such as 'image/jpeg'.
88        size : tuple
89            (width, height) in pixels.
90        transparent : bool
91            Optional. Transparent background if True.
92        bgcolor : string
93            Optional. Image background color.
94        method : string
95            Optional. HTTP DCP method name: Get or Post.
96       
97        Example
98        -------
99            >>> img = wms.getmap(layers=['global_mosaic'],
100            ...                  styles=['visual'],
101            ...                  srs='EPSG:4326',
102            ...                  bbox=(-112,36,-106,41),
103            ...                  format='image/jpeg',
104            ...                  size=(300,250),
105            ...                  transparent=True,
106            ...                  )
107            >>> out = open('example.jpg', 'wb')
108            >>> out.write(img.read())
109            >>> out.close()
110
111        """
112        md = self.capabilities
113        base_url = md.getOperationByName('GetMap').methods[method]['url']
114        request = {'version': self.version, 'request': 'GetMap'}
115       
116        # check layers and styles
117        assert len(layers) > 0
118        request['layers'] = ','.join(layers)
119        if styles:
120            assert len(styles) == len(layers)
121            request['styles'] = ','.join(styles)
122        else:
123            request['styles'] = ''
124
125        # size
126        request['width'] = str(size[0])
127        request['height'] = str(size[1])
128       
129        request['srs'] = str(srs)
130        request['bbox'] = ','.join([str(x) for x in bbox])
131        request['format'] = str(format)
132        request['transparent'] = str(transparent).upper()
133        request['bgcolor'] = '0x' + bgcolor[1:7]
134        request['exceptions'] = str(exceptions)
135       
136        data = urlencode(request)
137        if method == 'Post':
138            u = urlopen(base_url, data=data)
139        else:
140            u = urlopen(base_url + data)
141
142        # check for service exceptions, and return
143        if u.info()['Content-Type'] == 'application/vnd.ogc.se_xml':
144            se_xml = u.read()
145            se_tree = etree.fromstring(se_xml)
146            raise ServiceException, \
147                str(se_tree.find('ServiceException').text).strip()
148        return u
149
150    def getfeatureinfo(self):
151        raise NotImplementedError
152       
153       
154class ServiceMetadata(object):
155    """Abstraction for WMS metadata.
156   
157    Implements IServiceMetadata.
158    """
159
160    def __init__(self, infoset):
161        """Initialize from an element tree."""
162        self._root = infoset.getroot()
163        # properties
164        self.service = self._root.find('Service/Name').text
165        self.title = self._root.find('Service/Title').text
166        self.abstract = self._root.find('Service/Abstract').text
167        self.link = self._root.find('Service/OnlineResource').attrib.get('{http://www.w3.org/1999/xlink}href', '')
168       
169        # operations []
170        self.operations = []
171        for elem in self._root.findall('Capability/Request/*'):
172            self.operations.append(OperationMetadata(elem))
173
174        # exceptions
175        self.exceptions = [f.text for f \
176                in self._root.findall('Capability/Exception/Format')]
177       
178        # contents: our assumption is that services use a top-level layer
179        # as a metadata organizer, nothing more.
180        self.contents = []
181        top = self._root.find('Capability/Layer')
182        for elem in self._root.findall('Capability/Layer/Layer'):
183            self.contents.append(ContentMetadata(elem, top))
184
185        # keywords
186        self.keywords = [f.text for f in self._root.findall('Service/KeywordList/Keyword')]
187       
188        # contact person
189        self.provider = ContactMetadata(self._root.find('Service/ContactInformation'))
190       
191    def getContentByName(self, name):
192        """Return a named content item."""
193        for item in self.contents:
194            if item.name == name:
195                return item
196        raise KeyError, "No content named %s" % name
197
198    def getOperationByName(self, name):
199        """Return a named content item."""
200        for item in self.operations:
201            if item.name == name:
202                return item
203        raise KeyError, "No operation named %s" % name
204
205    #def toXML(self):
206    #    """x
207    #    """
208    #    top = etree.Element('a')
209    #    top.text = self.getName()
210    #    return etree.tostring(top)
211
212
213class ContentMetadata:
214    """Abstraction for WMS metadata.
215   
216    Implements IMetadata.
217    """
218
219    def __init__(self, elem, parent):
220        """Initialize."""
221        self.name = elem.find('Name').text
222        self.title = elem.find('Title').text
223        # bboxes
224        self.boundingBox = None
225        b = elem.find('BoundingBox')
226        if b is not None:
227            self.boundingBox = (float(b.attrib['minx']),float(b.attrib['miny']),
228                    float(b.attrib['maxx']), float(b.attrib['maxy']),
229                    b.attrib['SRS'])
230        else:
231            b = parent.find('BoundingBox')
232            if b is not None:
233                self.boundingBox = (
234                        float(b.attrib['minx']), float(b.attrib['miny']),
235                        float(b.attrib['maxx']), float(b.attrib['maxy']),
236                        b.attrib['SRS'])
237        self.boundingBoxWGS84 = None
238        b = elem.find('LatLonBoundingBox')
239        if b is not None:
240            self.boundingBoxWGS84 = (
241                    float(b.attrib['minx']),float(b.attrib['miny']),
242                    float(b.attrib['maxx']), float(b.attrib['maxy']),
243                    )
244        else:
245            b = parent.find('LatLonBoundingBox')
246            if b is not None:
247                self.boundingBoxWGS84 = (
248                        float(b.attrib['minx']), float(b.attrib['miny']),
249                        float(b.attrib['maxx']), float(b.attrib['maxy']),
250                        )
251        # crs options
252        self.crsOptions = [srs.text for srs in parent.findall('SRS')]
253
254        # styles
255        self.styles = dict([(s.find('Name').text, 
256                             {'title': s.find('Title').text}) \
257                             for s in elem.findall('Style')]
258                             )
259
260
261class OperationMetadata:
262    """Abstraction for WMS metadata.
263   
264    Implements IMetadata.
265    """
266    def __init__(self, elem):
267        """."""
268        self.name = elem.tag
269        # formatOptions
270        self.formatOptions = [f.text for f in elem.findall('Format')]
271        methods = []
272        for verb in elem.findall('DCPType/HTTP/*'):
273            url = verb.find('OnlineResource').attrib['{http://www.w3.org/1999/xlink}href']
274            methods.append((verb.tag, {'url': url}))
275        self.methods = dict(methods)
276
277
278class ContactMetadata:
279    """Abstraction for contact details advertised in GetCapabilities.
280    """
281
282    def __init__(self, elem):
283        self.name = elem.find('ContactPersonPrimary/ContactPerson').text
284        self.organization = elem.find('ContactPersonPrimary/ContactOrganization').text
285        address = elem.find('ContactAddress')
286        if address is not None:
287            try:   
288                self.address = address.find('Address').text
289                self.city = address.find('City').text
290                self.region = address.find('StateOrProvince').text
291                self.postcode = address.find('Postcode').text
292                self.country = address.find('Country').text
293            except: pass
294        self.email = elem.find('ContactElectronicMailAddress').text
295
296
297# Deprecated classes follow
298# TODO: remove
299
300class WMSCapabilitiesInfoset:
301    """High-level container for WMS Capabilities based on lxml.etree
302    """
303
304    def __init__(self, infoset):
305        """Initialize"""
306        self._infoset = infoset
307
308    def getroot(self):
309        return self._infoset
310
311    def getservice(self):
312        return self._infoset.find('Service')
313
314    def servicename(self):
315        e_service = self.getservice()
316        return e_service.find('Name').text
317
318    def servicetitle(self):
319        e_service = self.getservice()
320        return e_service.find('Title').text
321
322    def getmapformats(self):
323        e_getmap = self._infoset.find('Capability/Request/GetMap')
324        formats = ()
325        for f in e_getmap.getiterator('Format'):
326            formats = formats + (f.text,)
327        return formats
328
329    def layersrs(self):
330        e_layer = self._infoset.find('Capability/Layer')
331        srs = ()
332        for s in e_layer.getiterator('SRS'):
333            srs = srs + (s.text,)
334        return srs
335
336    def layernames(self):
337        names = ()
338        for n in self._infoset.findall('Capability/Layer/Layer/Name'):
339            names = names + (n.text,)
340        return names
341
342    def layertitles(self):
343        titles = ()
344        for n in self._infoset.findall('Capability/Layer/Layer/Title'):
345            titles = titles + (n.text,)
346        return titles
347
348    def getLayerInfo(self):
349        info = {}
350        for layer in self._infoset.findall('Capability/Layer/Layer'):
351            if layer.findall('Title'):
352                info[layer.findall('Title')[0].text] = layer.findall('Style')
353        return info
354
355    def bounds(self, name):
356        """Returns the bounds of the specified layer as a tuple.
357
358        Like (minx, miny, maxx, maxy, epsg)
359        """
360        top_layer = self._infoset.find('Capability/Layer')
361        for layer in top_layer.findall('Layer'):
362            n = layer.find('Name')
363            if n.text == name:
364                # First check for a BoundingBox
365                b = layer.find('BoundingBox')
366                if b is not None:
367                    return (float(b.attrib['minx']), float(b.attrib['miny']),
368                            float(b.attrib['maxx']), float(b.attrib['maxy']),
369                            b.attrib['SRS'])
370                else:
371                    b = layer.find('LatLonBoundingBox')
372                    #import pdb; pdb.set_trace()
373                    if b is not None:
374                        return (float(b.attrib['minx']),float(b.attrib['miny']),
375                                float(b.attrib['maxx']),float(b.attrib['maxy']),
376                                'EPSG:4326')
377                # Look at the top level layer
378                b = top_layer.find('BoundingBox')
379                if b is not None:
380                    return (float(b.attrib['minx']), float(b.attrib['miny']),
381                            float(b.attrib['maxx']), float(b.attrib['maxy']),
382                            b.attrib['SRS'])
383                else:
384                    b = top_layer.find('LatLonBoundingBox')
385                    if b is not None:
386                        return (float(b.attrib['minx']),float(b.attrib['miny']),
387                                float(b.attrib['maxx']),float(b.attrib['maxy']),
388                                'EPSG:4326')
389        # If we haven't returned a bbox, raise an exception
390        raise CapabilitiesError, "No bounding box specified for layer %s" % name
391               
392       
393class WMSCapabilitiesReader:
394    """Read and parse capabilities document into a lxml.etree infoset
395    """
396
397    def __init__(self, version='1.1.1'):
398        """Initialize"""
399        self.version = version
400        self._infoset = None
401
402    def capabilities_url(self, service_url):
403        """Return a capabilities url
404        """
405        qs = []
406        if service_url.find('?') != -1:
407            qs = cgi.parse_qsl(service_url.split('?')[1])
408
409        params = [x[0] for x in qs]
410
411        if 'service' not in params:
412            qs.append(('service', 'WMS'))
413        if 'request' not in params:
414            qs.append(('request', 'GetCapabilities'))
415        if 'version' not in params:
416            qs.append(('version', self.version))
417
418        urlqs = urlencode(tuple(qs))
419        return service_url.split('?')[0] + '?' + urlqs
420
421    def read(self, service_url):
422        """Get and parse a WMS capabilities document, returning an
423        instance of WMSCapabilitiesInfoset
424
425        service_url is the base url, to which is appended the service,
426        version, and request parameters
427        """
428        request = self.capabilities_url(service_url)
429        u = urlopen(request)
430        return WMSCapabilitiesInfoset(etree.fromstring(u.read()))
431
432    def readString(self, st):
433        """Parse a WMS capabilities document, returning an
434        instance of WMSCapabilitiesInfoset
435
436        string should be an XML capabilities document
437        """
438        if not isinstance(st, str):
439            raise ValueError("String must be of type string, not %s" % type(st))
440        return WMSCapabilitiesInfoset(etree.fromstring(st))
441
442
443class WMSError(Exception):
444    """Base class for WMS module errors
445    """
446
447    def __init__(self, message):
448        """Initialize a WMS Error"""
449        self.message = message
450
451    def toxml(self):
452        """Serialize into a WMS Service Exception XML
453        """
454        preamble = '<?xml version="1.0" ?>'
455        report_elem = etree.Element('ServiceExceptionReport')
456        report_elem.attrib['version'] = '1.1.1'
457        # Service Exception
458        exception_elem = etree.Element('ServiceException')
459        exception_elem.text = self.message
460        report_elem.append(exception_elem)
461        return preamble + etree.tostring(report_elem)
462
463
464
Note: See TracBrowser for help on using the repository browser.