source: cows/branches/cows_qesdi/cows/pylons/wms_controller.py @ 5493

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/branches/cows_qesdi/cows/pylons/wms_controller.py@5493
Revision 5493, 20.5 KB checked in by pnorton, 12 years ago (diff)

Implemented the changes made while working on the qesdi wms. Also changed the csmlbackend wms so that it works with the new WMSContoller.

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
22
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            if hasattr(layer, 'styles'):
156                styles = layer.styles
157            else:
158                styles = ['']
159               
160            # Create the cows object
161            ds = WmsDatasetSummary(identifier=layerName,
162                                   titles=[layer.title],
163                                   CRSs=layer.crss,
164                                   wgs84BoundingBoxes=[wgs84BBox],
165                                   boundingBoxes=bboxObjs,
166                                   abstracts=[layer.abstract],
167                                   dimensions=dims,
168                                   queryable=queryable,
169                                   dataURLs=dataURLs,
170                                   styles=styles)
171
172            # Stuff that should go in the capabilities tree eventually
173            ds.legendSize = layer.legendSize
174            ds.legendFormats = ['image/png']
175
176            c.capabilities.contents.datasetSummaries.append(ds)
177
178        # Add this operation here after we have found all formats
179        ows_controller.addOperation('GetFeatureInfo',
180                                    formats = list(featureInfoFormats))
181
182    def _getLayerParamInfo(self, paramName='layers'):
183        """
184        Retrieve the layers parameter enforcing the rule of only
185        selecting one layer.
186
187        @param paramName: Overrides the query string parameter name to
188            look for.  This is usefull for implementing GetFeatureInfo.
189
190        """
191        layerName = self.getOwsParam(paramName)
192
193        # Select the first layer if several are requested.
194        # This plays nicer with mapClient.
195        if ',' in layerName:
196            #layerName = layerName.split(',')[0]
197            raise InvalidParameterValue(
198                'Multi-layer GetLegend requests are not supported', 'layers')
199        try:
200            layerObj = self.layers[layerName]
201        except KeyError:
202            raise InvalidParameterValue('Layer %s not found' % layerName,
203                                        paramName)
204
205        return layerName, layerObj
206
207    def _getLayerParam(self, paramName='layers'):
208        """
209        Retrieve the layers parameter enforcing the rule of only
210        selecting one layer.
211
212        @param paramName: Overrides the query string parameter name to
213            look for.  This is usefull for implementing GetFeatureInfo.
214
215        """
216        layers = {}
217        layerNames = self.getOwsParam(paramName)
218       
219        # Select the first layer if several are requested.
220        # This plays nicer with mapClient.
221        layerNames = layerNames.split(',')
222       
223        layerObjects = []
224       
225        for layerName in layerNames:
226            try:
227                layerObj = self.layers[layerName]
228                layerObjects.append(layerObj)
229            except KeyError:
230                raise InvalidParameterValue('Layer %s not found' % layerName,
231                                        paramName)
232
233        return layerObjects
234
235    def _getFormatParam(self):
236        format = self.getOwsParam('format', default='image/png')
237        if format not in self._pilImageFormats:
238            raise InvalidParameterValue(
239                'Format %s not supported' % format, 'format')
240
241        return format
242
243    _escapedDimNames = ['width', 'height', 'version', 'request',
244                        'layers', 'styles', 'crs', 'srs', 'bbox',
245                        'format', 'transparent', 'bgcolor',
246                        'exceptions']
247
248    def _getDimValues(self, layerObj):
249        dimValues = {}
250        for dimName, dim in layerObj.dimensions.items():
251            defaultValue = dim.extent[0]
252            escapedDimName=self._mapDimToParam(dimName)
253            dimValues[escapedDimName] = self.getOwsParam(escapedDimName,
254                                                  default=defaultValue)
255        return dimValues
256
257    def _mapDimToParam(self, dimName):
258        """
259        Dimension names might clash with WMS parameter names, making
260        them inaccessible in WMS requests.  This method maps a
261        dimension name to a parameter name that appears in the
262        capabilities document and WMS requests.
263
264        """
265        if dimName.lower() in self._escapedDimNames:
266            return dimName+'_dim'
267        else:
268            return dimName
269       
270    def _mapParamToDim(self, dimParam):
271        """
272        Maps a dimension parameter name to it's real dimension name.
273
274        @see: _mapDimToParam()
275
276        """
277        try:
278            dimName = re.match(r'(.*)_dim$', dimParam).group(1)
279            if dimName.lower() in self._escapedDimNames:
280                return dimName
281            else:
282                return dimParam
283        except AttributeError:
284            return dimParam
285
286
287    def _retrieveSlab(self, layerObj, srs, style, dimValues, transparent, bgcolor, additionalParams):
288       
289        # Find the slab in the cache first
290        cacheKey = layerObj.getCacheKey(srs, style, dimValues, transparent, bgcolor, additionalParams)
291        slab = self._layerSlabCache.get(cacheKey)
292       
293        if slab is None:
294           
295            slab = layerObj.getSlab(srs, style, dimValues, transparent, bgcolor, additionalParams)
296           
297            if cacheKey is not None:
298                self._layerSlabCache[cacheKey] = slab
299
300        return slab
301
302    #-------------------------------------------------------------------------
303    # OWS Operation methods
304   
305    def GetMap(self):
306
307        # Get the parameters
308        version      = self._getVersionParam()
309        format       = self._getFormatParam()       
310        transparent  = self._getTransparentParam()
311        bgcolor      = self._getBgcolorParam()
312        bbox         = self._getBboxParam()
313        width        = self._getWidthParam()
314        height       = self._getHeightParam()
315       
316        layerObjects = self._getLayerParam()
317       
318        styles       = self._getStylesParam(len(layerObjects))
319        srs          = self._getSrsParam(version)
320       
321        log.debug("layerNames = %s" % ([o.name for o in layerObjects],))
322       
323        finalImg = Image.new('RGBA', (width, height), (0,0,0,0))
324
325        # Multiple Layers handling.. 
326        for i in range(len(layerObjects)):
327            layerObj = layerObjects[i]
328           
329            #if no styles provided, set style = ""           
330            if styles == "":
331                style = ""
332            else:
333                style = styles[i]
334           
335            if srs not in layerObj.crss:
336                raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerObj.name, srs))
337
338            dimValues = self._getDimValues(layerObj)
339           
340            #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
341            restoredDimValues={}
342            for dim in dimValues:
343                restoredDim=self._mapParamToDim(dim)
344                restoredDimValues[restoredDim]=dimValues[dim]
345           
346            expectedParams = []
347            expectedParams.extend(self._escapedDimNames)
348            expectedParams.extend(layerObj.dimensions.keys())
349           
350            #get any other parameters on the request that the layer might need
351            additionalParams = self._getAdditionalParameters(expectedParams)
352           
353            slab = self._retrieveSlab(layerObj, srs, style, dimValues, 
354                                      transparent, bgcolor, additionalParams)
355
356            img = slab.getImage(bbox, width, height)
357           
358            finalImg = Image.composite(finalImg, img, finalImg)   
359
360        # IE < 7 doesn't display the alpha layer right.  Here we sniff the
361        # user agent and remove the alpha layer if necessary.
362        try:
363            ua = request.headers['User-Agent']
364        except:
365            pass
366        else:
367            if 'MSIE' in ua and 'MSIE 7' not in ua:
368                finalImg = finalImg.convert('RGB')
369
370        self._writeImageResponse(finalImg, format)
371
372
373    def GetContext(self):
374        """
375        Return a WebMap Context document for a given set of layers.
376
377        """
378        # Parameters
379        layers = self.getOwsParam('layers', default=None)
380        format = self.getOwsParam('format', default='text/xml')
381
382        # Filter self.layers for selected layers
383        if layers is not None:
384            newLayerMap = {}
385            for layerName in layers.split(','):
386                try:
387                    newLayerMap[layerName] = self.layers[layerName]
388                except KeyError:
389                    raise InvalidParameterValue('Layer %s not found' % layerName,
390                                                'layers')
391                   
392            self.layers = newLayerMap
393
394        # Automatically select the first bbox/crs for the first layer
395        aLayer = self.layers.values()[0]
396        crs = aLayer.crss[0]
397        bb = aLayer.getBBox(crs)
398        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
399
400        # Initialise as if doing GetCapabilities
401        ows_controller.initCapabilities()
402        self._loadCapabilities()
403
404        if format == 'text/xml':
405            response.headers['Content-Type'] = format
406            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
407            return t.generate(c=c).render()
408        elif format == 'application/json':
409            response.headers['Content-Type'] = format
410            t = ows_controller.templateLoader.load('wms_context_json.txt',
411                                                   cls=NewTextTemplate)
412            return t.generate(c=c).render()
413        else:
414            raise InvalidParameterValue('Format %s not supported' % format)
415
416    def GetFeatureInfo(self):
417        # Housekeeping
418        version = self.getOwsParam('version', default=self.validVersions[0])
419        if version not in self.validVersions:
420            raise InvalidParameterValue('Version %s not supported' % version,
421                                        'version')
422
423        # Coordinate parameters
424        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
425        width = int(self.getOwsParam('width'))
426        height = int(self.getOwsParam('height'))
427         
428        # Get pixel location
429        i = int(self.getOwsParam('i'))
430        j = int(self.getOwsParam('j'))
431
432        # Translate to geo-coordinates
433        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
434        #start preparing GetFeatureInfo response. Assumes "HTML" output format
435
436        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
437       
438       
439        layers = self._getLayerParam('query_layers')
440        #Adjusts response for multiple layers
441        if len(layers) > 1:
442            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
443 
444        htmlResponse = htmlResponse+"<ul>"
445       
446        format = self.getOwsParam('info_format', default='text/html')
447        for layerName, layerObj in layers.iteritems():
448            log.debug('Format: %s' % format)
449            log.debug('Title: %s' % layerObj.title)
450            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
451        if format not in layerObj.featureInfoFormats:
452            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
453
454        if version == '1.1.1':
455                srs = self.getOwsParam('srs')
456        else:
457            srs = self.getOwsParam('crs')
458
459        if srs not in layerObj.crss:
460            raise InvalidParameterValue('Layer %s does not support SRS %s' %
461                                        (layerName, srs))
462
463        # Dimension handling
464        dimValues = {}
465        for dimName, dim in layerObj.dimensions.items():
466            defaultValue = dim.extent[0]
467            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
468       
469        response.headers['Content-Type'] = format
470        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
471
472    def GetLegend(self):
473        """
474        Return an image of the legend.
475
476        """
477        # Parameters
478        layerName, layerObj = self._getLayerParamInfo()
479        format = self._getFormatParam()
480
481        # This hook alows extra arguments to be passed to the layer backend.
482        additionalParams = self._getAdditionalParameters(['format'])
483       
484        img = layerObj.getLegendImage(renderOpts=additionalParams)
485       
486        self._writeImageResponse(img, format)
487
488
489
490    def GetInfo(self):
491        from pprint import pformat
492        request.headers['Content-Type'] = 'text/ascii'
493        response.write('Some info about this service\n')
494        for layer in model.ukcip02.layers:
495            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
496
497           
498    def _getAdditionalParameters(self, expectedParams):
499       
500        additionalParams = {}
501       
502        for paramName, paramValue in self._owsParams.items():
503           
504            paramName = paramName.lower()
505                       
506            #ignore any of the expected parameters
507            if paramName in [p.lower() for p in expectedParams]:
508                continue
509           
510            additionalParams[paramName] = paramValue
511           
512        return additionalParams
513   
514    def _getStylesParam(self, numLayers):
515        styles = self.getOwsParam('styles', default="")
516       
517        if styles != "":
518            styles = styles.split(',')
519           
520            assert len(styles) == numLayers, \
521               "Number of styles %s didn't match the number of layers %s" % ( len(styles), numLayers)
522
523        return styles
524
525    def _getTransparentParam(self):
526        transparent = self.getOwsParam('transparent', default='FALSE')
527        return transparent.lower() == 'true'
528   
529    def _getBgcolorParam(self):
530        return self.getOwsParam('bgcolor', default='0xFFFFFF')
531
532    def _getVersionParam(self):
533        version = self.getOwsParam('version', default=self.validVersions[0])
534       
535        if version not in self.validVersions:
536            raise InvalidParameterValue('Version %s not supported' % version, 'version')
537       
538        return version
539
540    def _getSrsParam(self, version):
541        if version == '1.1.1':
542            srs = self.getOwsParam('srs')
543        else:
544            srs = self.getOwsParam('crs')
545           
546        return srs
547
548    def _getBboxParam(self):
549        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
550        return bbox
551   
552    def _getWidthParam(self):
553        return int(self.getOwsParam('width'))
554   
555    def _getHeightParam(self):
556        return int(self.getOwsParam('height'))
557   
558
559    def _writeImageResponse(self, pilImage, format):
560       
561        buf = StringIO()
562        pilImage.save(buf, self._pilImageFormats[format])
563
564        response.headers['Content-Type'] = format
565        response.write(buf.getvalue())   
Note: See TracBrowser for help on using the repository browser.