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

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

Merging in qesdi changes to cows trunk - still need to merge new backend.

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