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

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

Improved the csml_data_reader so that it no longer assumes the feature id is the variable name.

Improved the data reader layer so that it now gets the dataset name from the csml file.

Added some code to the geoplot slabs that is used for selecting a log scale.

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, layerNames = %s' % (layerName,
248                                        self.layers.keys()))
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       
475        layerObj = layers[0]
476       
477        if format not in layerObj.featureInfoFormats:
478            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
479
480        if version == '1.1.1':
481                srs = self.getOwsParam('srs')
482        else:
483            srs = self.getOwsParam('crs')
484
485        if srs not in layerObj.crss:
486            raise InvalidParameterValue('Layer %s does not support SRS %s' %
487                                        (layerName, srs))
488
489        # Dimension handling
490        dimValues = {}
491        for dimName, dim in layerObj.dimensions.items():
492            defaultValue = dim.extent[0]
493            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
494       
495        response.headers['Content-Type'] = format
496        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
497
498    def GetLegend(self):
499        """
500        Return an image of the legend.
501
502        """
503        # Parameters
504        layerName, layerObj = self._getLayerParamInfo()
505        format = self._getFormatParam()
506
507        # This hook alows extra arguments to be passed to the layer backend.
508        additionalParams = self._getAdditionalParameters(['format'])
509       
510        dimValues = self._getDimValues(layerObj)
511        #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
512        restoredDimValues={}
513        for dim in dimValues:
514            restoredDim=self._mapParamToDim(dim)
515            restoredDimValues[restoredDim]=dimValues[dim]
516       
517        img = layerObj.getLegendImage(restoredDimValues, renderOpts=additionalParams)
518       
519        self._writeImageResponse(img, format)
520
521
522
523    def GetInfo(self):
524        from pprint import pformat
525        request.headers['Content-Type'] = 'text/ascii'
526        response.write('Some info about this service\n')
527        for layer in model.ukcip02.layers:
528            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
529
530           
531    def _getAdditionalParameters(self, expectedParams):
532       
533        additionalParams = {}
534       
535        for paramName, paramValue in self._owsParams.items():
536           
537            paramName = paramName.lower()
538                       
539            #ignore any of the expected parameters
540            if paramName in [p.lower() for p in expectedParams]:
541                continue
542           
543            additionalParams[paramName] = paramValue
544           
545        return additionalParams
546   
547    def _getStylesParam(self, numLayers):
548        styles = self.getOwsParam('styles', default="")
549       
550        if styles != "":
551            styles = styles.split(',')
552           
553            assert len(styles) == numLayers, \
554               "Number of styles %s didn't match the number of layers %s" % ( len(styles), numLayers)
555
556        return styles
557
558    def _getTransparentParam(self):
559        transparent = self.getOwsParam('transparent', default='FALSE')
560        return transparent.lower() == 'true'
561   
562    def _getBgcolorParam(self):
563        return self.getOwsParam('bgcolor', default='0xFFFFFF')
564
565    def _getVersionParam(self):
566        version = self.getOwsParam('version', default=self.validVersions[0])
567       
568        if version not in self.validVersions:
569            raise InvalidParameterValue('Version %s not supported' % version, 'version')
570       
571        return version
572
573    def _getSrsParam(self, version):
574        if version == '1.1.1':
575            srs = self.getOwsParam('srs')
576        else:
577            srs = self.getOwsParam('crs')
578           
579        return srs
580
581    def _getBboxParam(self):
582        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
583        return bbox
584   
585    def _getWidthParam(self):
586        return int(self.getOwsParam('width'))
587   
588    def _getHeightParam(self):
589        return int(self.getOwsParam('height'))
590   
591
592    def _writeImageResponse(self, pilImage, format):
593       
594        buf = StringIO()
595        pilImage.save(buf, self._pilImageFormats[format])
596
597        response.headers['Content-Type'] = format
598       
599        if config.get('cows.browser_caching_enabled','').lower() == 'true':
600            response.headers["cache-control"] = "public, max-age=3600"
601            response.headers["pragma"] = ""
602                   
603        response.write(buf.getvalue())   
Note: See TracBrowser for help on using the repository browser.