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

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

modifying wxs capabilities to optionally read service identification title from layermapper class

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