source: cows/trunk/cows/pylons/wms_controller.py @ 5755

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/trunk/cows/pylons/wms_controller.py@5755
Revision 5755, 21.3 KB checked in by pnorton, 11 years ago (diff)

Modified the browser sniffing code so that it dosen't trigger for IE 8. Also made some changes to the slab option parsers.

Line 
1# BSD Licence
2# Copyright (c) 2009, Science & Technology Facilities Council (STFC)
3# All rights reserved.
4#
5# See the LICENSE file in the source distribution of this software for
6# the full license text.
7
8"""
9WMS controller for OGC Web Services (OWS).
10
11@author: Stephen Pascoe
12"""
13
14import re
15import math
16from cStringIO import StringIO
17from sets import Set
18from matplotlib.cm import get_cmap
19from pylons import request, response, c
20from routes import url_for
21from routes.util import GenerationException
22from string import upper
23import logging
24log = logging.getLogger(__name__)
25
26try: 
27    from PIL import Image
28except ImportError:
29    import Image
30
31from genshi.template import NewTextTemplate
32
33from cows.model.wms import WmsDatasetSummary, Dimension, DataURL
34from cows.model import PossibleValues, WGS84BoundingBox, BoundingBox, Contents
35from cows.pylons import ows_controller
36from cows.exceptions import *
37from cows import bbox_util
38
39class WMSController(ows_controller.OWSController):
40    """
41    Subclass this controller in a pylons application and set the layerMapper
42    class attribute to implement a WMS.
43
44    @cvar layerMapper: an cows.service.wms_iface.ILayerMapper object.
45
46    """
47    layerMapper = None
48    #layers = {}   
49    _pilImageFormats = {
50        'image/png': 'PNG',
51        'image/jpg': 'JPEG',
52        'image/gif': 'GIF',
53        'image/tiff': 'TIFF'
54        }
55    _layerSlabCache = {}
56
57    #-------------------------------------------------------------------------
58    # Attributes required by OWSController
59
60    service = 'WMS'
61    owsOperations = (ows_controller.OWSController.owsOperations +
62        ['GetMap', 'GetContext', 'GetLegend', 'GetFeatureInfo', 'GetInfo'])
63    validVersions = ['1.1.1', '1.3.0']
64
65    #-------------------------------------------------------------------------
66
67    def __before__(self, **kwargs):
68        """
69        This default implementation of __before__() will pass all routes
70        arguments to the layer mapper to retrieve a list of layers for
71        this WMS.
72
73        It will be called automatically by pylons before each action method.
74
75        @todo: The layer mapper needs to come from somewhere.
76
77        """
78        #self.updateSequence = "hello"
79        log.debug("loading layers")
80        #print self.layers
81        self.layers = self.layerMapper.map(**kwargs)
82
83    #-------------------------------------------------------------------------
84    # Methods implementing stubs in OWSController
85
86    def _renderCapabilities(self, version, format):
87        if format == 'application/json':
88            t = ows_controller.templateLoader.load('wms_capabilities_json.txt',
89                                                   cls=NewTextTemplate)
90        elif version == '1.1.1':
91            t = ows_controller.templateLoader.load('wms_capabilities_1_1_1.xml')
92        elif version == '1.3.0':
93            t = ows_controller.templateLoader.load('wms_capabilities_1_3_0.xml')
94        else:
95            # We should never get here!  The framework should raise an exception before now.
96            raise RuntimeError("Version %s not supported" % version)
97       
98        return t.generate(c=c).render()
99
100    def _loadCapabilities(self):
101        """
102        @note: Assumes self.layers has already been created by __before__().
103
104        """
105        #!TODO: Add json format to GetCapabilities operation
106
107        ows_controller.addOperation('GetMap', formats=self._pilImageFormats.keys())
108        ows_controller.addOperation('GetContext', formats=['text/xml', 'application/json'])
109        ows_controller.addOperation('GetLegend',
110                                    formats=['image/png'])
111        ows_controller.addOperation('GetInfo')
112       
113        featureInfoFormats = Set()
114
115        log.debug('Loading capabilities contents')
116        c.capabilities.contents = Contents()
117        for layerName, layer in self.layers.items():
118            log.debug('LayerName: %s' % layerName)
119            log.debug('Loading layer %s' % layerName)
120
121            wgs84BBox = WGS84BoundingBox(layer.wgs84BBox[:2],
122                                         layer.wgs84BBox[2:])
123            # Get CRS/BBOX pairs
124            bboxObjs = []
125            for crs in layer.crss:
126                bbox = layer.getBBox(crs)
127                bboxObjs.append(BoundingBox(bbox[:2], bbox[2:], crs=crs))
128            # Get dimensions
129            dims = {}
130            for dimName, dim in layer.dimensions.items():
131                dimParam = self._mapDimToParam(dimName)
132                dims[dimParam] = Dimension(valuesUnit=dim.units,
133                                          unitSymbol=dim.units,
134                                          possibleValues=
135                                            PossibleValues.fromAllowedValues(dim.extent))
136            # Does the layer implement GetFeatureInfo?
137            if layer.featureInfoFormats:
138                queryable = True
139                featureInfoFormats.union_update(layer.featureInfoFormats)
140            else:
141                queryable = False
142               
143            #URL to WCS - uses named route 'wcsroute'
144            #TODO: Allow for a WCS blacklist to opt out of providing dataurls for certain datasets?
145            #TODO: How to make this more configurable - what if WCS is not coupled with WMS?
146            try:
147                version='1.0.0' #wcs version
148                wcsbaseurl=url_for('wcsroute', fileoruri=c.fileoruri,qualified=True)+'?'
149                dataURLs=[DataURL(format='WCS:CoverageDescription', onlineResource='%sService=WCS&Request=DescribeCoverage&Coverage=%s&Version=%s'%(wcsbaseurl, layerName, version))]
150            except GenerationException:
151                log.info("dataURLs not populated: could not generate WCS url with url_for('wcsroute', filedoruri=%s,qualified=True)"%c.fileoruri)
152                dataURLs=[]
153               
154           
155           
156           
157            if hasattr(layer, 'styles'):
158                styles = layer.styles
159            else:
160                styles = ['']
161           
162            if hasattr(layer, 'metadataURLs'):
163                metadataURLs = layer.metadataURLs
164            else:
165                metadataURLs = []
166           
167            # Create the cows object
168            ds = WmsDatasetSummary(identifier=layerName,
169                                   titles=[layer.title],
170                                   CRSs=layer.crss,
171                                   wgs84BoundingBoxes=[wgs84BBox],
172                                   boundingBoxes=bboxObjs,
173                                   abstracts=[layer.abstract],
174                                   dimensions=dims,
175                                   queryable=queryable,
176                                   dataURLs=dataURLs,
177                                   styles=styles,
178                                   metadataURLs=metadataURLs)
179
180            # Stuff that should go in the capabilities tree eventually
181            ds.legendSize = layer.legendSize
182            ds.legendFormats = ['image/png']
183
184            c.capabilities.contents.datasetSummaries.append(ds)
185
186        # Add this operation here after we have found all formats
187        ows_controller.addOperation('GetFeatureInfo',
188                                    formats = list(featureInfoFormats))
189
190    def _getLayerParamInfo(self, paramName='layers'):
191        """
192        Retrieve the layers parameter enforcing the rule of only
193        selecting one layer.
194
195        @param paramName: Overrides the query string parameter name to
196            look for.  This is usefull for implementing GetFeatureInfo.
197
198        """
199        layerName = self.getOwsParam(paramName)
200
201        # Select the first layer if several are requested.
202        # This plays nicer with mapClient.
203        if ',' in layerName:
204            #layerName = layerName.split(',')[0]
205            raise InvalidParameterValue(
206                'Multi-layer GetLegend requests are not supported', 'layers')
207        try:
208            layerObj = self.layers[layerName]
209        except KeyError:
210            raise InvalidParameterValue('Layer %s not found' % layerName,
211                                        paramName)
212
213        return layerName, layerObj
214
215    def _getLayerParam(self, paramName='layers'):
216        """
217        Retrieve the layers parameter enforcing the rule of only
218        selecting one layer.
219
220        @param paramName: Overrides the query string parameter name to
221            look for.  This is usefull for implementing GetFeatureInfo.
222
223        """
224        layers = {}
225        layerNames = self.getOwsParam(paramName)
226       
227        # Select the first layer if several are requested.
228        # This plays nicer with mapClient.
229        layerNames = layerNames.split(',')
230       
231        layerObjects = []
232       
233        for layerName in layerNames:
234            try:
235                layerObj = self.layers[layerName]
236                layerObjects.append(layerObj)
237            except KeyError:
238                raise InvalidParameterValue('Layer %s not found' % layerName,
239                                        paramName)
240
241        return layerObjects
242
243    def _getFormatParam(self):
244        format = self.getOwsParam('format', default='image/png')
245        if format not in self._pilImageFormats:
246            raise InvalidParameterValue(
247                'Format %s not supported' % format, 'format')
248
249        return format
250
251    _escapedDimNames = ['width', 'height', 'version', 'request',
252                        'layers', 'styles', 'crs', 'srs', 'bbox',
253                        'format', 'transparent', 'bgcolor',
254                        'exceptions']
255
256    def _getDimValues(self, layerObj):
257        dimValues = {}
258        for dimName, dim in layerObj.dimensions.items():
259            defaultValue = dim.extent[0]
260            escapedDimName=self._mapDimToParam(dimName)
261            dimValues[escapedDimName] = self.getOwsParam(escapedDimName,
262                                                  default=defaultValue)
263        return dimValues
264
265    def _mapDimToParam(self, dimName):
266        """
267        Dimension names might clash with WMS parameter names, making
268        them inaccessible in WMS requests.  This method maps a
269        dimension name to a parameter name that appears in the
270        capabilities document and WMS requests.
271
272        """
273        if dimName.lower() in self._escapedDimNames:
274            return dimName+'_dim'
275        else:
276            return dimName
277       
278    def _mapParamToDim(self, dimParam):
279        """
280        Maps a dimension parameter name to it's real dimension name.
281
282        @see: _mapDimToParam()
283
284        """
285        try:
286            dimName = re.match(r'(.*)_dim$', dimParam).group(1)
287            if dimName.lower() in self._escapedDimNames:
288                return dimName
289            else:
290                return dimParam
291        except AttributeError:
292            return dimParam
293
294
295    def _retrieveSlab(self, layerObj, srs, style, dimValues, transparent, bgcolor, additionalParams):
296       
297        # Find the slab in the cache first
298        cacheKey = layerObj.getCacheKey(srs, style, dimValues, transparent, bgcolor, additionalParams)
299        slab = self._layerSlabCache.get(cacheKey)
300       
301        if slab is None:
302           
303            slab = layerObj.getSlab(srs, style, dimValues, transparent, bgcolor, additionalParams)
304           
305            if cacheKey is not None:
306                self._layerSlabCache[cacheKey] = slab
307
308        return slab
309
310    #-------------------------------------------------------------------------
311    # OWS Operation methods
312   
313    def GetMap(self):
314
315        # Get the parameters
316        version      = self._getVersionParam()
317        format       = self._getFormatParam()       
318        transparent  = self._getTransparentParam()
319        bgcolor      = self._getBgcolorParam()
320        bbox         = self._getBboxParam()
321        width        = self._getWidthParam()
322        height       = self._getHeightParam()
323       
324        layerObjects = self._getLayerParam()
325       
326        styles       = self._getStylesParam(len(layerObjects))
327        srs          = self._getSrsParam(version)
328       
329        log.debug("layerNames = %s" % ([o.name for o in layerObjects],))
330       
331        finalImg = Image.new('RGBA', (width, height), (0,0,0,0))
332
333        # Multiple Layers handling.. 
334        for i in range(len(layerObjects)):
335            layerObj = layerObjects[i]
336           
337                       
338            #if no styles  provided, set style = ""           
339            if styles =="":
340                style = ""
341            else:
342                style = styles[i]
343                           
344            #if style parameter is "default", set style = ""
345            if upper(style) == 'DEFAULT':
346                style=""
347           
348            if srs not in layerObj.crss:
349                raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerObj.name, srs))
350
351            dimValues = self._getDimValues(layerObj)
352           
353            #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
354            restoredDimValues={}
355            for dim in dimValues:
356                restoredDim=self._mapParamToDim(dim)
357                restoredDimValues[restoredDim]=dimValues[dim]
358           
359            expectedParams = []
360            expectedParams.extend(self._escapedDimNames)
361            expectedParams.extend(layerObj.dimensions.keys())
362           
363            #get any other parameters on the request that the layer might need
364            additionalParams = self._getAdditionalParameters(expectedParams)
365           
366            slab = self._retrieveSlab(layerObj, srs, style, restoredDimValues, 
367                                      transparent, bgcolor, additionalParams)
368
369            img = slab.getImage(bbox, width, height)
370           
371            finalImg = Image.composite(finalImg, img, finalImg)   
372
373        # IE 6.0 doesn't display the alpha layer right.  Here we sniff the
374        # user agent and remove the alpha layer if necessary.
375        try:
376            ua = request.headers['User-Agent']
377            log.debug("ua = %s" % (ua,))
378        except:
379            pass
380        else:
381            if 'MSIE 6.0' in ua:
382                finalImg = finalImg.convert('RGB')
383
384        self._writeImageResponse(finalImg, format)
385
386
387    def GetContext(self):
388        """
389        Return a WebMap Context document for a given set of layers.
390
391        """
392        # Parameters
393        layers = self.getOwsParam('layers', default=None)
394        format = self.getOwsParam('format', default='text/xml')
395
396        # Filter self.layers for selected layers
397        if layers is not None:
398            newLayerMap = {}
399            for layerName in layers.split(','):
400                try:
401                    newLayerMap[layerName] = self.layers[layerName]
402                except KeyError:
403                    raise InvalidParameterValue('Layer %s not found' % layerName,
404                                                'layers')
405                   
406            self.layers = newLayerMap
407
408        # Automatically select the first bbox/crs for the first layer
409        aLayer = self.layers.values()[0]
410        crs = aLayer.crss[0]
411        bb = aLayer.getBBox(crs)
412        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
413
414        # Initialise as if doing GetCapabilities
415        ows_controller.initCapabilities()
416        self._loadCapabilities()
417
418        if format == 'text/xml':
419            response.headers['Content-Type'] = format
420            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
421            return t.generate(c=c).render()
422        elif format == 'application/json':
423            response.headers['Content-Type'] = format
424            t = ows_controller.templateLoader.load('wms_context_json.txt',
425                                                   cls=NewTextTemplate)
426            return t.generate(c=c).render()
427        else:
428            raise InvalidParameterValue('Format %s not supported' % format)
429
430    def GetFeatureInfo(self):
431        # Housekeeping
432        version = self.getOwsParam('version', default=self.validVersions[0])
433        if version not in self.validVersions:
434            raise InvalidParameterValue('Version %s not supported' % version,
435                                        'version')
436
437        # Coordinate parameters
438        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
439        width = int(self.getOwsParam('width'))
440        height = int(self.getOwsParam('height'))
441         
442        # Get pixel location
443        i = int(self.getOwsParam('i'))
444        j = int(self.getOwsParam('j'))
445
446        # Translate to geo-coordinates
447        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
448        #start preparing GetFeatureInfo response. Assumes "HTML" output format
449
450        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
451       
452       
453        layers = self._getLayerParam('query_layers')
454        #Adjusts response for multiple layers
455        if len(layers) > 1:
456            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
457 
458        htmlResponse = htmlResponse+"<ul>"
459       
460        format = self.getOwsParam('info_format', default='text/html')
461        for layerName, layerObj in layers.iteritems():
462            log.debug('Format: %s' % format)
463            log.debug('Title: %s' % layerObj.title)
464            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
465        if format not in layerObj.featureInfoFormats:
466            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
467
468        if version == '1.1.1':
469                srs = self.getOwsParam('srs')
470        else:
471            srs = self.getOwsParam('crs')
472
473        if srs not in layerObj.crss:
474            raise InvalidParameterValue('Layer %s does not support SRS %s' %
475                                        (layerName, srs))
476
477        # Dimension handling
478        dimValues = {}
479        for dimName, dim in layerObj.dimensions.items():
480            defaultValue = dim.extent[0]
481            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
482       
483        response.headers['Content-Type'] = format
484        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
485
486    def GetLegend(self):
487        """
488        Return an image of the legend.
489
490        """
491        # Parameters
492        layerName, layerObj = self._getLayerParamInfo()
493        format = self._getFormatParam()
494
495        # This hook alows extra arguments to be passed to the layer backend.
496        additionalParams = self._getAdditionalParameters(['format'])
497       
498        dimValues = self._getDimValues(layerObj)
499        #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
500        restoredDimValues={}
501        for dim in dimValues:
502            restoredDim=self._mapParamToDim(dim)
503            restoredDimValues[restoredDim]=dimValues[dim]
504       
505        img = layerObj.getLegendImage(restoredDimValues, renderOpts=additionalParams)
506       
507        self._writeImageResponse(img, format)
508
509
510
511    def GetInfo(self):
512        from pprint import pformat
513        request.headers['Content-Type'] = 'text/ascii'
514        response.write('Some info about this service\n')
515        for layer in model.ukcip02.layers:
516            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
517
518           
519    def _getAdditionalParameters(self, expectedParams):
520       
521        additionalParams = {}
522       
523        for paramName, paramValue in self._owsParams.items():
524           
525            paramName = paramName.lower()
526                       
527            #ignore any of the expected parameters
528            if paramName in [p.lower() for p in expectedParams]:
529                continue
530           
531            additionalParams[paramName] = paramValue
532           
533        return additionalParams
534   
535    def _getStylesParam(self, numLayers):
536        styles = self.getOwsParam('styles', default="")
537       
538        if styles != "":
539            styles = styles.split(',')
540           
541            assert len(styles) == numLayers, \
542               "Number of styles %s didn't match the number of layers %s" % ( len(styles), numLayers)
543
544        return styles
545
546    def _getTransparentParam(self):
547        transparent = self.getOwsParam('transparent', default='FALSE')
548        return transparent.lower() == 'true'
549   
550    def _getBgcolorParam(self):
551        return self.getOwsParam('bgcolor', default='0xFFFFFF')
552
553    def _getVersionParam(self):
554        version = self.getOwsParam('version', default=self.validVersions[0])
555       
556        if version not in self.validVersions:
557            raise InvalidParameterValue('Version %s not supported' % version, 'version')
558       
559        return version
560
561    def _getSrsParam(self, version):
562        if version == '1.1.1':
563            srs = self.getOwsParam('srs')
564        else:
565            srs = self.getOwsParam('crs')
566           
567        return srs
568
569    def _getBboxParam(self):
570        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
571        return bbox
572   
573    def _getWidthParam(self):
574        return int(self.getOwsParam('width'))
575   
576    def _getHeightParam(self):
577        return int(self.getOwsParam('height'))
578   
579
580    def _writeImageResponse(self, pilImage, format):
581       
582        buf = StringIO()
583        pilImage.save(buf, self._pilImageFormats[format])
584
585        response.headers['Content-Type'] = format
586        response.write(buf.getvalue())   
Note: See TracBrowser for help on using the repository browser.