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

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

Restored dimension values also needed fixing in get legend.

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 < 7 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        except:
378            pass
379        else:
380            if 'MSIE' in ua and 'MSIE 7' not in ua:
381                finalImg = finalImg.convert('RGB')
382
383        self._writeImageResponse(finalImg, format)
384
385
386    def GetContext(self):
387        """
388        Return a WebMap Context document for a given set of layers.
389
390        """
391        # Parameters
392        layers = self.getOwsParam('layers', default=None)
393        format = self.getOwsParam('format', default='text/xml')
394
395        # Filter self.layers for selected layers
396        if layers is not None:
397            newLayerMap = {}
398            for layerName in layers.split(','):
399                try:
400                    newLayerMap[layerName] = self.layers[layerName]
401                except KeyError:
402                    raise InvalidParameterValue('Layer %s not found' % layerName,
403                                                'layers')
404                   
405            self.layers = newLayerMap
406
407        # Automatically select the first bbox/crs for the first layer
408        aLayer = self.layers.values()[0]
409        crs = aLayer.crss[0]
410        bb = aLayer.getBBox(crs)
411        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
412
413        # Initialise as if doing GetCapabilities
414        ows_controller.initCapabilities()
415        self._loadCapabilities()
416
417        if format == 'text/xml':
418            response.headers['Content-Type'] = format
419            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
420            return t.generate(c=c).render()
421        elif format == 'application/json':
422            response.headers['Content-Type'] = format
423            t = ows_controller.templateLoader.load('wms_context_json.txt',
424                                                   cls=NewTextTemplate)
425            return t.generate(c=c).render()
426        else:
427            raise InvalidParameterValue('Format %s not supported' % format)
428
429    def GetFeatureInfo(self):
430        # Housekeeping
431        version = self.getOwsParam('version', default=self.validVersions[0])
432        if version not in self.validVersions:
433            raise InvalidParameterValue('Version %s not supported' % version,
434                                        'version')
435
436        # Coordinate parameters
437        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
438        width = int(self.getOwsParam('width'))
439        height = int(self.getOwsParam('height'))
440         
441        # Get pixel location
442        i = int(self.getOwsParam('i'))
443        j = int(self.getOwsParam('j'))
444
445        # Translate to geo-coordinates
446        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
447        #start preparing GetFeatureInfo response. Assumes "HTML" output format
448
449        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
450       
451       
452        layers = self._getLayerParam('query_layers')
453        #Adjusts response for multiple layers
454        if len(layers) > 1:
455            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
456 
457        htmlResponse = htmlResponse+"<ul>"
458       
459        format = self.getOwsParam('info_format', default='text/html')
460        for layerName, layerObj in layers.iteritems():
461            log.debug('Format: %s' % format)
462            log.debug('Title: %s' % layerObj.title)
463            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
464        if format not in layerObj.featureInfoFormats:
465            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
466
467        if version == '1.1.1':
468                srs = self.getOwsParam('srs')
469        else:
470            srs = self.getOwsParam('crs')
471
472        if srs not in layerObj.crss:
473            raise InvalidParameterValue('Layer %s does not support SRS %s' %
474                                        (layerName, srs))
475
476        # Dimension handling
477        dimValues = {}
478        for dimName, dim in layerObj.dimensions.items():
479            defaultValue = dim.extent[0]
480            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
481       
482        response.headers['Content-Type'] = format
483        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
484
485    def GetLegend(self):
486        """
487        Return an image of the legend.
488
489        """
490        # Parameters
491        layerName, layerObj = self._getLayerParamInfo()
492        format = self._getFormatParam()
493
494        # This hook alows extra arguments to be passed to the layer backend.
495        additionalParams = self._getAdditionalParameters(['format'])
496       
497        dimValues = self._getDimValues(layerObj)
498        #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
499        restoredDimValues={}
500        for dim in dimValues:
501            restoredDim=self._mapParamToDim(dim)
502            restoredDimValues[restoredDim]=dimValues[dim]
503       
504        img = layerObj.getLegendImage(restoredDimValues, renderOpts=additionalParams)
505       
506        self._writeImageResponse(img, format)
507
508
509
510    def GetInfo(self):
511        from pprint import pformat
512        request.headers['Content-Type'] = 'text/ascii'
513        response.write('Some info about this service\n')
514        for layer in model.ukcip02.layers:
515            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
516
517           
518    def _getAdditionalParameters(self, expectedParams):
519       
520        additionalParams = {}
521       
522        for paramName, paramValue in self._owsParams.items():
523           
524            paramName = paramName.lower()
525                       
526            #ignore any of the expected parameters
527            if paramName in [p.lower() for p in expectedParams]:
528                continue
529           
530            additionalParams[paramName] = paramValue
531           
532        return additionalParams
533   
534    def _getStylesParam(self, numLayers):
535        styles = self.getOwsParam('styles', default="")
536       
537        if styles != "":
538            styles = styles.split(',')
539           
540            assert len(styles) == numLayers, \
541               "Number of styles %s didn't match the number of layers %s" % ( len(styles), numLayers)
542
543        return styles
544
545    def _getTransparentParam(self):
546        transparent = self.getOwsParam('transparent', default='FALSE')
547        return transparent.lower() == 'true'
548   
549    def _getBgcolorParam(self):
550        return self.getOwsParam('bgcolor', default='0xFFFFFF')
551
552    def _getVersionParam(self):
553        version = self.getOwsParam('version', default=self.validVersions[0])
554       
555        if version not in self.validVersions:
556            raise InvalidParameterValue('Version %s not supported' % version, 'version')
557       
558        return version
559
560    def _getSrsParam(self, version):
561        if version == '1.1.1':
562            srs = self.getOwsParam('srs')
563        else:
564            srs = self.getOwsParam('crs')
565           
566        return srs
567
568    def _getBboxParam(self):
569        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
570        return bbox
571   
572    def _getWidthParam(self):
573        return int(self.getOwsParam('width'))
574   
575    def _getHeightParam(self):
576        return int(self.getOwsParam('height'))
577   
578
579    def _writeImageResponse(self, pilImage, format):
580       
581        buf = StringIO()
582        pilImage.save(buf, self._pilImageFormats[format])
583
584        response.headers['Content-Type'] = format
585        response.write(buf.getvalue())   
Note: See TracBrowser for help on using the repository browser.