source: cows/branches/migrate-py26-pylons10/cows/pylons/wms_controller.py @ 7342

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/branches/migrate-py26-pylons10/cows/pylons/wms_controller.py@7342
Revision 7342, 24.2 KB checked in by spascoe, 9 years ago (diff)

New branch for migration to Python-2.6 and Pylons-1.0.

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, config, url
20from pylons import tmpl_context as c
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('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('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        for l in layerList:
297            # does this layer match?
298            if l.title == name:
299                layerObj = l
300                break
301           
302            # does this layer have a child that matches?
303            if name.find(l.title + "_") == 0:
304               
305                reducedName = name[len(l.title + "_"):]
306               
307                found = self._searchLayerChildren(l.childLayers, reducedName)
308               
309                if found is not None:
310                    layerObj = found
311                    break
312
313        return layerObj
314
315
316    def _getFormatParam(self):
317        format = self.getOwsParam('format', default='image/png')
318        if format not in self._pilImageFormats:
319            raise InvalidParameterValue(
320                'Format %s not supported' % format, 'format')
321
322        return format
323
324    _escapedDimNames = ['width', 'height', 'version', 'request',
325                        'layers', 'styles', 'crs', 'srs', 'bbox',
326                        'format', 'transparent', 'bgcolor',
327                        'exceptions']
328
329    def _getDimValues(self, layerObj):
330        dimValues = {}
331        for dimName, dim in layerObj.dimensions.items():
332            defaultValue = dim.extent[0]
333            escapedDimName=self._mapDimToParam(dimName)
334            dimValues[escapedDimName] = self.getOwsParam(escapedDimName,
335                                                  default=defaultValue)
336        return dimValues
337
338    def _mapDimToParam(self, dimName):
339        """
340        Dimension names might clash with WMS parameter names, making
341        them inaccessible in WMS requests.  This method maps a
342        dimension name to a parameter name that appears in the
343        capabilities document and WMS requests.
344
345        """
346        if dimName.lower() in self._escapedDimNames:
347            return dimName+'_dim'
348        else:
349            return dimName
350       
351    def _mapParamToDim(self, dimParam):
352        """
353        Maps a dimension parameter name to it's real dimension name.
354
355        @see: _mapDimToParam()
356
357        """
358        try:
359            dimName = re.match(r'(.*)_dim$', dimParam).group(1)
360            if dimName.lower() in self._escapedDimNames:
361                return dimName
362            else:
363                return dimParam
364        except AttributeError:
365            return dimParam
366
367
368    def _retrieveSlab(self, layerObj, srs, style, dimValues, transparent, bgcolor, additionalParams):
369       
370        # Find the slab in the cache first
371        cacheKey = layerObj.getCacheKey(srs, style, dimValues, transparent, bgcolor, additionalParams)
372        slab = self._layerSlabCache.get(cacheKey)
373       
374        if slab is None:
375           
376            slab = layerObj.getSlab(srs, style, dimValues, transparent, bgcolor, additionalParams)
377           
378            if cacheKey is not None:
379                self._layerSlabCache[cacheKey] = slab
380
381        return slab
382
383    #-------------------------------------------------------------------------
384    # OWS Operation methods
385   
386    def GetMap(self):
387
388        # Get the parameters
389        version      = self._getVersionParam()
390        format       = self._getFormatParam()       
391        transparent  = self._getTransparentParam()
392        bgcolor      = self._getBgcolorParam()
393        bbox         = self._getBboxParam()
394        width        = self._getWidthParam()
395        height       = self._getHeightParam()
396       
397        layerObjects = self._getLayerParam()
398       
399        styles       = self._getStylesParam(len(layerObjects))
400        srs          = self._getSrsParam(version)
401       
402        log.debug("layerNames = %s" % ([o.name for o in layerObjects],))
403       
404        finalImg = Image.new('RGBA', (width, height), (0,0,0,0))
405
406        # Multiple Layers handling.. 
407        for i in range(len(layerObjects)):
408            layerObj = layerObjects[i]
409           
410                       
411            #if no styles  provided, set style = ""           
412            if styles =="":
413                style = ""
414            else:
415                style = styles[i]
416                           
417            #if style parameter is "default", set style = ""
418            if upper(style) == 'DEFAULT':
419                style=""
420           
421            if srs not in layerObj.crss:
422                raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerObj.name, srs))
423
424            dimValues = self._getDimValues(layerObj)
425           
426            #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
427            restoredDimValues={}
428            for dim in dimValues:
429                restoredDim=self._mapParamToDim(dim)
430                restoredDimValues[restoredDim]=dimValues[dim]
431           
432            expectedParams = []
433            expectedParams.extend(self._escapedDimNames)
434            expectedParams.extend(layerObj.dimensions.keys())
435           
436            #get any other parameters on the request that the layer might need
437            additionalParams = self._getAdditionalParameters(expectedParams)
438           
439            slab = self._retrieveSlab(layerObj, srs, style, restoredDimValues, 
440                                      transparent, bgcolor, additionalParams)
441
442            img = slab.getImage(bbox, width, height)
443           
444            finalImg = Image.composite(finalImg, img, finalImg)   
445
446        # IE 6.0 doesn't display the alpha layer right.  Here we sniff the
447        # user agent and remove the alpha layer if necessary.
448        try:
449            ua = request.headers['User-Agent']
450            log.debug("ua = %s" % (ua,))
451        except:
452            pass
453        else:
454            if 'MSIE 6.0' in ua:
455                finalImg = finalImg.convert('RGB')
456
457        self._writeImageResponse(finalImg, format)
458
459
460    def GetContext(self):
461        """
462        Return a WebMap Context document for a given set of layers.
463
464        """
465        # Parameters
466        layers = self.getOwsParam('layers', default=None)
467        format = self.getOwsParam('format', default='text/xml')
468
469        # Filter self.layers for selected layers
470        if layers is not None:
471            newLayerMap = {}
472            for layerName in layers.split(','):
473                try:
474                    newLayerMap[layerName] = self.layers[layerName]
475                except KeyError:
476                    raise InvalidParameterValue('Layer %s not found' % layerName,
477                                                'layers')
478                   
479            self.layers = newLayerMap
480
481        # Automatically select the first bbox/crs for the first layer
482        aLayer = self.layers.values()[0]
483        crs = aLayer.crss[0]
484        bb = aLayer.getBBox(crs)
485        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
486
487        # Initialise as if doing GetCapabilities
488        ows_controller.initCapabilities()
489        self._loadCapabilities()
490
491        if format == 'text/xml':
492            response.headers['Content-Type'] = format
493            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
494            return t.generate(c=c).render()
495        elif format == 'application/json':
496            response.headers['Content-Type'] = format
497            t = ows_controller.templateLoader.load('wms_context_json.txt',
498                                                   cls=NewTextTemplate)
499            return t.generate(c=c).render()
500        else:
501            raise InvalidParameterValue('Format %s not supported' % format)
502
503    def GetFeatureInfo(self):
504        # Housekeeping
505        version = self.getOwsParam('version', default=self.validVersions[0])
506        if version not in self.validVersions:
507            raise InvalidParameterValue('Version %s not supported' % version,
508                                        'version')
509
510        # Coordinate parameters
511        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
512        width = int(self.getOwsParam('width'))
513        height = int(self.getOwsParam('height'))
514         
515        # Get pixel location
516        i = int(self.getOwsParam('i'))
517        j = int(self.getOwsParam('j'))
518
519        # Translate to geo-coordinates
520        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
521        #start preparing GetFeatureInfo response. Assumes "HTML" output format
522
523        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
524       
525       
526        layers = self._getLayerParam('query_layers')
527        #Adjusts response for multiple layers
528        if len(layers) > 1:
529            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
530 
531        htmlResponse = htmlResponse+"<ul>"
532       
533        format = self.getOwsParam('info_format', default='text/html')
534        for layerName, layerObj in layers.iteritems():
535            log.debug('Format: %s' % format)
536            log.debug('Title: %s' % layerObj.title)
537            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
538       
539        layerObj = layers[0]
540       
541        if format not in layerObj.featureInfoFormats:
542            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
543
544        if version == '1.1.1':
545                srs = self.getOwsParam('srs')
546        else:
547            srs = self.getOwsParam('crs')
548
549        if srs not in layerObj.crss:
550            raise InvalidParameterValue('Layer %s does not support SRS %s' %
551                                        (layerName, srs))
552
553        # Dimension handling
554        dimValues = {}
555        for dimName, dim in layerObj.dimensions.items():
556            defaultValue = dim.extent[0]
557            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
558       
559        response.headers['Content-Type'] = format
560        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
561
562    def GetLegend(self):
563        """
564        Return an image of the legend.
565
566        """
567        # Parameters
568        layerName, layerObj = self._getLayerParamInfo()
569        format = self._getFormatParam()
570
571        # This hook alows extra arguments to be passed to the layer backend.
572        additionalParams = self._getAdditionalParameters(['format'])
573       
574        dimValues = self._getDimValues(layerObj)
575        #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
576        restoredDimValues={}
577        for dim in dimValues:
578            restoredDim=self._mapParamToDim(dim)
579            restoredDimValues[restoredDim]=dimValues[dim]
580       
581        layerObjects = self._getLayerParam()
582        styles       = self._getStylesParam(len(layerObjects))       
583       
584       
585        #can't cope with multiple styles so just taking the first one
586        if len(styles) > 0:
587            style = styles[0]
588        else:
589            style = None
590       
591        img = layerObj.getLegendImage(restoredDimValues, 
592                                      renderOpts=additionalParams,
593                                      style=style)
594       
595        self._writeImageResponse(img, format)
596
597
598
599    def GetInfo(self):
600        from pprint import pformat
601        request.headers['Content-Type'] = 'text/ascii'
602        response.write('Some info about this service\n')
603        for layer in model.ukcip02.layers:
604            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
605
606           
607    def _getAdditionalParameters(self, expectedParams):
608       
609        additionalParams = {}
610       
611        for paramName, paramValue in self._owsParams.items():
612           
613            paramName = paramName.lower()
614                       
615            #ignore any of the expected parameters
616            if paramName in [p.lower() for p in expectedParams]:
617                continue
618           
619            additionalParams[paramName] = paramValue
620           
621        return additionalParams
622   
623    def _getStylesParam(self, numLayers):
624        styles = self.getOwsParam('styles', default="")
625       
626        if styles != "":
627            styles = styles.split(',')
628           
629            assert len(styles) == numLayers, \
630               "Number of styles %s didn't match the number of layers %s" % ( len(styles), numLayers)
631
632        return styles
633
634    def _getTransparentParam(self):
635        transparent = self.getOwsParam('transparent', default='FALSE')
636        return transparent.lower() == 'true'
637   
638    def _getBgcolorParam(self):
639        return self.getOwsParam('bgcolor', default='0xFFFFFF')
640
641    def _getVersionParam(self):
642        version = self.getOwsParam('version', default=self.validVersions[0])
643       
644        if version not in self.validVersions:
645            raise InvalidParameterValue('Version %s not supported' % version, 'version')
646       
647        return version
648
649    def _getSrsParam(self, version):
650        if version == '1.1.1':
651            srs = self.getOwsParam('srs')
652        else:
653            srs = self.getOwsParam('crs')
654           
655        return srs
656
657    def _getBboxParam(self):
658        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
659        return bbox
660   
661    def _getWidthParam(self):
662        return int(self.getOwsParam('width'))
663   
664    def _getHeightParam(self):
665        return int(self.getOwsParam('height'))
666   
667
668    def _writeImageResponse(self, pilImage, format):
669       
670        buf = StringIO()
671        pilImage.save(buf, self._pilImageFormats[format])
672
673        response.headers['Content-Type'] = format
674       
675        if config.get('cows.browser_caching_enabled','').lower() == 'true':
676            response.headers["cache-control"] = "public, max-age=3600"
677            response.headers["pragma"] = ""
678                   
679        response.write(buf.getvalue())   
Note: See TracBrowser for help on using the repository browser.