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

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

First attempt at trying to implement a folder structure for the csml files.

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   
64    validVersions = ['1.1.1', '1.3.0']
65
66    #-------------------------------------------------------------------------
67
68    def __before__(self, **kwargs):
69        """
70        This default implementation of __before__() will pass all routes
71        arguments to the layer mapper to retrieve a list of layers for
72        this WMS.
73
74        It will be called automatically by pylons before each action method.
75
76        @todo: The layer mapper needs to come from somewhere.
77
78        """
79        #self.updateSequence = "hello"
80        log.debug("loading layers")
81        #print self.layers
82        self.layers = self.layerMapper.map(**kwargs)
83
84    #-------------------------------------------------------------------------
85    # Methods implementing stubs in OWSController
86
87    def _renderCapabilities(self, version, format):
88        if format == 'application/json':
89            t = ows_controller.templateLoader.load('wms_capabilities_json.txt',
90                                                   cls=NewTextTemplate)
91        elif version == '1.1.1':
92            t = ows_controller.templateLoader.load('wms_capabilities_1_1_1.xml')
93        elif version == '1.3.0':
94            t = ows_controller.templateLoader.load('wms_capabilities_1_3_0.xml')
95        else:
96            # We should never get here!  The framework should raise an exception before now.
97            raise RuntimeError("Version %s not supported" % version)
98       
99        return t.generate(c=c).render()
100
101    def _loadCapabilities(self):
102        """
103        @note: Assumes self.layers has already been created by __before__().
104
105        """
106        #!TODO: Add json format to GetCapabilities operation
107
108        ows_controller.addOperation('GetMap', formats=self._pilImageFormats.keys())
109        ows_controller.addOperation('GetContext', formats=['text/xml', 'application/json'])
110        ows_controller.addOperation('GetLegend',
111                                    formats=['image/png'])
112        ows_controller.addOperation('GetInfo')
113       
114        featureInfoFormats = Set()
115
116        log.debug('Loading capabilities contents')
117        c.capabilities.contents = Contents()
118               
119        for layerName, layer in self.layers.items():
120
121            ds = self._buildWMSDatasetSummary(layer, featureInfoFormats)
122           
123            c.capabilities.contents.datasetSummaries.append(ds)
124       
125       
126        log.debug('Setting dataset name')
127       
128        #LayerMapper may optionally implement a datasetName attribute which
129        #will be tried if serviceIdentification/title is not supplied in capabilities config
130        if c.capabilities.serviceIdentification.titles[0] is None:
131                try:
132                    c.capabilities.serviceIdentification.titles=[self.layerMapper.datasetName]
133                except AttributeError:
134                    pass
135       
136        # if there is still no serviceIdentification, use the fileoruri
137        if c.capabilities.serviceIdentification.titles[0] is None:
138            fileoruri = request.environ['pylons.routes_dict']['fileoruri']
139            c.capabilities.serviceIdentification.titles=[fileoruri]
140               
141        # Add this operation here after we have found all formats
142        ows_controller.addOperation('GetFeatureInfo',
143                                    formats = list(featureInfoFormats))
144
145
146    def _buildWMSDatasetSummary(self, layer, featureInfoFormats):
147       
148        if hasattr(layer, 'wgs84BBox'):
149            wgs84BBox = WGS84BoundingBox(layer.wgs84BBox[:2],
150                                         layer.wgs84BBox[2:])
151        else:
152            wgs84BBox = None
153           
154        # Get CRS/BBOX pairs
155        bboxObjs = []
156       
157        if hasattr(layer, 'crss') and layer.crss is not None:
158            for crs in layer.crss:
159                bbox = layer.getBBox(crs)
160                bboxObjs.append(BoundingBox(bbox[:2], bbox[2:], crs=crs))
161               
162        # Get dimensions
163        dims = {}
164       
165        if hasattr(layer, 'dimensions') and layer.dimensions is not None:
166            for dimName, dim in layer.dimensions.items():
167                dimParam = self._mapDimToParam(dimName)
168                dims[dimParam] = Dimension(valuesUnit=dim.units, unitSymbol=dim.units,
169                                          possibleValues=
170                                            PossibleValues.fromAllowedValues(dim.extent))
171           
172           
173        # Does the layer implement GetFeatureInfo?
174        if layer.featureInfoFormats:
175            queryable = True
176            featureInfoFormats.union_update(layer.featureInfoFormats)
177        else:
178            queryable = False
179           
180        if layer.name != None:
181            #URL to WCS - uses named route 'wcsroute'
182            #TODO: Allow for a WCS blacklist to opt out of providing dataurls for certain datasets?
183            #TODO: How to make this more configurable - what if WCS is not coupled with WMS?
184            try:
185                version='1.0.0' #wcs version
186                wcsbaseurl=url_for('wcsroute', fileoruri=c.fileoruri,qualified=True)+'?'
187                dataURLs=[DataURL(format='WCS:CoverageDescription', onlineResource='%sService=WCS&Request=DescribeCoverage&Coverage=%s&Version=%s'%(wcsbaseurl, layer.name, version))]
188            except GenerationException:
189                log.info("dataURLs not populated: could not generate WCS url with url_for('wcsroute', filedoruri=%s,qualified=True)"%c.fileoruri)
190                dataURLs=[]
191        else:
192            dataURLs = []
193       
194       
195        if hasattr(layer, 'styles'):
196            styles = layer.styles
197        else:
198            styles = ['']
199       
200        if hasattr(layer, 'metadataURLs'):
201            metadataURLs = layer.metadataURLs
202        else:
203            metadataURLs = []
204       
205        children = []
206       
207        if hasattr(layer, 'childLayers'):
208            children = [self._buildWMSDatasetSummary(l, featureInfoFormats) \
209                                                     for l in layer.childLayers]
210       
211        # Create the cows object
212        ds = WmsDatasetSummary(identifier=layer.name,
213                               titles=[layer.title],
214                               CRSs=layer.crss,
215                               wgs84BoundingBoxes=[wgs84BBox],
216                               boundingBoxes=bboxObjs,
217                               abstracts=[layer.abstract],
218                               dimensions=dims,
219                               queryable=queryable,
220                               dataURLs=dataURLs,
221                               styles=styles,
222                               children=children,
223                               metadataURLs=metadataURLs)
224
225        # Stuff that should go in the capabilities tree eventually
226        ds.legendSize = layer.legendSize
227        ds.legendFormats = ['image/png']       
228
229        return ds
230
231    def _getLayerParamInfo(self, paramName='layers'):
232        """
233        Retrieve the layers parameter enforcing the rule of only
234        selecting one layer.
235
236        @param paramName: Overrides the query string parameter name to
237            look for.  This is usefull for implementing GetFeatureInfo.
238
239        """
240        layerName = self.getOwsParam(paramName)
241
242        # Select the first layer if several are requested.
243        # This plays nicer with mapClient.
244        if ',' in layerName:
245            raise InvalidParameterValue(
246                'Multi-layer GetLegend requests are not supported', 'layers')
247           
248        layerObj = self._getLayerFromMap(layerName)
249       
250        return layerName, layerObj
251   
252    def _getLayerParam(self, paramName='layers'):
253        """
254        Retrieve the layers parameters.
255
256        @param paramName: Overrides the query string parameter name to
257            look for.  This is useful for implementing GetFeatureInfo.
258
259        """
260        layerNames = self.getOwsParam(paramName)
261       
262        layerNames = layerNames.split(',')
263       
264        layerObjects = [ self._getLayerFromMap(name) for name in layerNames]
265       
266        return layerObjects
267   
268    def _getLayerFromMap(self, layerName):
269        """
270        Searches the layer map for the named layer, if no matching layer
271        is found an InvalidParameterValue is raised.
272        """
273       
274        layerObj = None
275       
276        if layerName in self.layers:
277            layerObj = self.layers[layerName]
278        else:
279           
280            fileoruri = request.environ['pylons.routes_dict']['fileoruri']
281            log.debug("fileoruri = %s" % (fileoruri,))
282            if layerName.find(fileoruri + "_") == 0:
283               
284                reducedName = layerName[len(fileoruri + "_") :]
285                layerObj = self._searchLayerChildren(self.layers.values(), reducedName)
286       
287        if layerObj is None:
288            raise InvalidParameterValue('Layer %s not found, layerNames = %s' 
289                                        % (layerName, self.layers.keys()))
290       
291        return layerObj
292   
293    def _searchLayerChildren(self, layerList, name):
294        layerObj = None
295       
296        log.debug("looking for name %s in layers %s" % (name, [l.title for l in layerList] ,))
297       
298        for l in layerList:
299            # does this layer match?
300            if l.title == name:
301                layerObj = l
302                break
303           
304            # does this layer have a child that matches?
305            if name.find(l.title + "_") == 0:
306               
307                reducedName = name[len(l.title + "_"):]
308               
309                found = self._searchLayerChildren(l.childLayers, reducedName)
310               
311                if found is not None:
312                    layerObj = found
313                    break
314
315        log.debug("found layerObj = %s" % ( layerObj,))
316        return layerObj
317
318
319    def _getFormatParam(self):
320        format = self.getOwsParam('format', default='image/png')
321        if format not in self._pilImageFormats:
322            raise InvalidParameterValue(
323                'Format %s not supported' % format, 'format')
324
325        return format
326
327    _escapedDimNames = ['width', 'height', 'version', 'request',
328                        'layers', 'styles', 'crs', 'srs', 'bbox',
329                        'format', 'transparent', 'bgcolor',
330                        'exceptions']
331
332    def _getDimValues(self, layerObj):
333        dimValues = {}
334        for dimName, dim in layerObj.dimensions.items():
335            defaultValue = dim.extent[0]
336            escapedDimName=self._mapDimToParam(dimName)
337            dimValues[escapedDimName] = self.getOwsParam(escapedDimName,
338                                                  default=defaultValue)
339        return dimValues
340
341    def _mapDimToParam(self, dimName):
342        """
343        Dimension names might clash with WMS parameter names, making
344        them inaccessible in WMS requests.  This method maps a
345        dimension name to a parameter name that appears in the
346        capabilities document and WMS requests.
347
348        """
349        if dimName.lower() in self._escapedDimNames:
350            return dimName+'_dim'
351        else:
352            return dimName
353       
354    def _mapParamToDim(self, dimParam):
355        """
356        Maps a dimension parameter name to it's real dimension name.
357
358        @see: _mapDimToParam()
359
360        """
361        try:
362            dimName = re.match(r'(.*)_dim$', dimParam).group(1)
363            if dimName.lower() in self._escapedDimNames:
364                return dimName
365            else:
366                return dimParam
367        except AttributeError:
368            return dimParam
369
370
371    def _retrieveSlab(self, layerObj, srs, style, dimValues, transparent, bgcolor, additionalParams):
372       
373        # Find the slab in the cache first
374        cacheKey = layerObj.getCacheKey(srs, style, dimValues, transparent, bgcolor, additionalParams)
375        slab = self._layerSlabCache.get(cacheKey)
376       
377        if slab is None:
378           
379            slab = layerObj.getSlab(srs, style, dimValues, transparent, bgcolor, additionalParams)
380           
381            if cacheKey is not None:
382                self._layerSlabCache[cacheKey] = slab
383
384        return slab
385
386    #-------------------------------------------------------------------------
387    # OWS Operation methods
388   
389    def GetMap(self):
390
391        # Get the parameters
392        version      = self._getVersionParam()
393        format       = self._getFormatParam()       
394        transparent  = self._getTransparentParam()
395        bgcolor      = self._getBgcolorParam()
396        bbox         = self._getBboxParam()
397        width        = self._getWidthParam()
398        height       = self._getHeightParam()
399       
400        layerObjects = self._getLayerParam()
401       
402        styles       = self._getStylesParam(len(layerObjects))
403        srs          = self._getSrsParam(version)
404       
405        log.debug("layerNames = %s" % ([o.name for o in layerObjects],))
406       
407        finalImg = Image.new('RGBA', (width, height), (0,0,0,0))
408
409        # Multiple Layers handling.. 
410        for i in range(len(layerObjects)):
411            layerObj = layerObjects[i]
412           
413                       
414            #if no styles  provided, set style = ""           
415            if styles =="":
416                style = ""
417            else:
418                style = styles[i]
419                           
420            #if style parameter is "default", set style = ""
421            if upper(style) == 'DEFAULT':
422                style=""
423           
424            if srs not in layerObj.crss:
425                raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerObj.name, srs))
426
427            dimValues = self._getDimValues(layerObj)
428           
429            #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
430            restoredDimValues={}
431            for dim in dimValues:
432                restoredDim=self._mapParamToDim(dim)
433                restoredDimValues[restoredDim]=dimValues[dim]
434           
435            expectedParams = []
436            expectedParams.extend(self._escapedDimNames)
437            expectedParams.extend(layerObj.dimensions.keys())
438           
439            #get any other parameters on the request that the layer might need
440            additionalParams = self._getAdditionalParameters(expectedParams)
441           
442            slab = self._retrieveSlab(layerObj, srs, style, restoredDimValues, 
443                                      transparent, bgcolor, additionalParams)
444
445            img = slab.getImage(bbox, width, height)
446           
447            finalImg = Image.composite(finalImg, img, finalImg)   
448
449        # IE 6.0 doesn't display the alpha layer right.  Here we sniff the
450        # user agent and remove the alpha layer if necessary.
451        try:
452            ua = request.headers['User-Agent']
453            log.debug("ua = %s" % (ua,))
454        except:
455            pass
456        else:
457            if 'MSIE 6.0' in ua:
458                finalImg = finalImg.convert('RGB')
459
460        self._writeImageResponse(finalImg, format)
461
462
463    def GetContext(self):
464        """
465        Return a WebMap Context document for a given set of layers.
466
467        """
468        # Parameters
469        layers = self.getOwsParam('layers', default=None)
470        format = self.getOwsParam('format', default='text/xml')
471
472        # Filter self.layers for selected layers
473        if layers is not None:
474            newLayerMap = {}
475            for layerName in layers.split(','):
476                try:
477                    newLayerMap[layerName] = self.layers[layerName]
478                except KeyError:
479                    raise InvalidParameterValue('Layer %s not found' % layerName,
480                                                'layers')
481                   
482            self.layers = newLayerMap
483
484        # Automatically select the first bbox/crs for the first layer
485        aLayer = self.layers.values()[0]
486        crs = aLayer.crss[0]
487        bb = aLayer.getBBox(crs)
488        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
489
490        # Initialise as if doing GetCapabilities
491        ows_controller.initCapabilities()
492        self._loadCapabilities()
493
494        if format == 'text/xml':
495            response.headers['Content-Type'] = format
496            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
497            return t.generate(c=c).render()
498        elif format == 'application/json':
499            response.headers['Content-Type'] = format
500            t = ows_controller.templateLoader.load('wms_context_json.txt',
501                                                   cls=NewTextTemplate)
502            return t.generate(c=c).render()
503        else:
504            raise InvalidParameterValue('Format %s not supported' % format)
505
506    def GetFeatureInfo(self):
507        # Housekeeping
508        version = self.getOwsParam('version', default=self.validVersions[0])
509        if version not in self.validVersions:
510            raise InvalidParameterValue('Version %s not supported' % version,
511                                        'version')
512
513        # Coordinate parameters
514        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
515        width = int(self.getOwsParam('width'))
516        height = int(self.getOwsParam('height'))
517         
518        # Get pixel location
519        i = int(self.getOwsParam('i'))
520        j = int(self.getOwsParam('j'))
521
522        # Translate to geo-coordinates
523        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
524        #start preparing GetFeatureInfo response. Assumes "HTML" output format
525
526        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
527       
528       
529        layers = self._getLayerParam('query_layers')
530        #Adjusts response for multiple layers
531        if len(layers) > 1:
532            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
533 
534        htmlResponse = htmlResponse+"<ul>"
535       
536        format = self.getOwsParam('info_format', default='text/html')
537        for layerName, layerObj in layers.iteritems():
538            log.debug('Format: %s' % format)
539            log.debug('Title: %s' % layerObj.title)
540            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
541       
542        layerObj = layers[0]
543       
544        if format not in layerObj.featureInfoFormats:
545            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
546
547        if version == '1.1.1':
548                srs = self.getOwsParam('srs')
549        else:
550            srs = self.getOwsParam('crs')
551
552        if srs not in layerObj.crss:
553            raise InvalidParameterValue('Layer %s does not support SRS %s' %
554                                        (layerName, srs))
555
556        # Dimension handling
557        dimValues = {}
558        for dimName, dim in layerObj.dimensions.items():
559            defaultValue = dim.extent[0]
560            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
561       
562        response.headers['Content-Type'] = format
563        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
564
565    def GetLegend(self):
566        """
567        Return an image of the legend.
568
569        """
570        # Parameters
571        layerName, layerObj = self._getLayerParamInfo()
572        format = self._getFormatParam()
573
574        # This hook alows extra arguments to be passed to the layer backend.
575        additionalParams = self._getAdditionalParameters(['format'])
576       
577        dimValues = self._getDimValues(layerObj)
578        #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
579        restoredDimValues={}
580        for dim in dimValues:
581            restoredDim=self._mapParamToDim(dim)
582            restoredDimValues[restoredDim]=dimValues[dim]
583       
584        layerObjects = self._getLayerParam()
585        styles       = self._getStylesParam(len(layerObjects))       
586       
587       
588        #can't cope with multiple styles so just taking the first one
589        if len(styles) > 0:
590            style = styles[0]
591        else:
592            style = None
593       
594        img = layerObj.getLegendImage(restoredDimValues, 
595                                      renderOpts=additionalParams,
596                                      style=style)
597       
598        self._writeImageResponse(img, format)
599
600
601
602    def GetInfo(self):
603        from pprint import pformat
604        request.headers['Content-Type'] = 'text/ascii'
605        response.write('Some info about this service\n')
606        for layer in model.ukcip02.layers:
607            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
608
609           
610    def _getAdditionalParameters(self, expectedParams):
611       
612        additionalParams = {}
613       
614        for paramName, paramValue in self._owsParams.items():
615           
616            paramName = paramName.lower()
617                       
618            #ignore any of the expected parameters
619            if paramName in [p.lower() for p in expectedParams]:
620                continue
621           
622            additionalParams[paramName] = paramValue
623           
624        return additionalParams
625   
626    def _getStylesParam(self, numLayers):
627        styles = self.getOwsParam('styles', default="")
628       
629        if styles != "":
630            styles = styles.split(',')
631           
632            assert len(styles) == numLayers, \
633               "Number of styles %s didn't match the number of layers %s" % ( len(styles), numLayers)
634
635        return styles
636
637    def _getTransparentParam(self):
638        transparent = self.getOwsParam('transparent', default='FALSE')
639        return transparent.lower() == 'true'
640   
641    def _getBgcolorParam(self):
642        return self.getOwsParam('bgcolor', default='0xFFFFFF')
643
644    def _getVersionParam(self):
645        version = self.getOwsParam('version', default=self.validVersions[0])
646       
647        if version not in self.validVersions:
648            raise InvalidParameterValue('Version %s not supported' % version, 'version')
649       
650        return version
651
652    def _getSrsParam(self, version):
653        if version == '1.1.1':
654            srs = self.getOwsParam('srs')
655        else:
656            srs = self.getOwsParam('crs')
657           
658        return srs
659
660    def _getBboxParam(self):
661        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
662        return bbox
663   
664    def _getWidthParam(self):
665        return int(self.getOwsParam('width'))
666   
667    def _getHeightParam(self):
668        return int(self.getOwsParam('height'))
669   
670
671    def _writeImageResponse(self, pilImage, format):
672       
673        buf = StringIO()
674        pilImage.save(buf, self._pilImageFormats[format])
675
676        response.headers['Content-Type'] = format
677       
678        if config.get('cows.browser_caching_enabled','').lower() == 'true':
679            response.headers["cache-control"] = "public, max-age=3600"
680            response.headers["pragma"] = ""
681                   
682        response.write(buf.getvalue())   
Note: See TracBrowser for help on using the repository browser.