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

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

Improved the legend min and max handling in the csml backend, also changed the controller slightly to pass the dimensions to the getLegendImage function.

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