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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/trunk/cows/pylons/wms_controller.py@5424
Revision 5424, 20.0 KB checked in by spascoe, 10 years ago (diff)

Removing tabs from python source

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
22
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            # Create the cows object
154            ds = WmsDatasetSummary(identifier=layerName,
155                                   titles=[layer.title],
156                                   CRSs=layer.crss,
157                                   wgs84BoundingBoxes=[wgs84BBox],
158                                   boundingBoxes=bboxObjs,
159                                   abstracts=[layer.abstract],
160                                   dimensions=dims,
161                                   queryable=queryable,
162                                   dataURLs=dataURLs)
163
164            # Stuff that should go in the capabilities tree eventually
165            ds.legendSize = layer.legendSize
166            ds.legendFormats = ['image/png']
167
168            c.capabilities.contents.datasetSummaries.append(ds)
169
170        # Add this operation here after we have found all formats
171        ows_controller.addOperation('GetFeatureInfo',
172                                    formats = list(featureInfoFormats))
173
174    def _getLayerParamInfo(self, paramName='layers'):
175        """
176        Retrieve the layers parameter enforcing the rule of only
177        selecting one layer.
178
179        @param paramName: Overrides the query string parameter name to
180            look for.  This is usefull for implementing GetFeatureInfo.
181
182        """
183        layerName = self.getOwsParam(paramName)
184
185        # Select the first layer if several are requested.
186        # This plays nicer with mapClient.
187        if ',' in layerName:
188            #layerName = layerName.split(',')[0]
189            raise InvalidParameterValue(
190                'Multi-layer GetLegend requests are not supported', 'layers')
191        try:
192            layerObj = self.layers[layerName]
193        except KeyError:
194            raise InvalidParameterValue('Layer %s not found' % layerName,
195                                        paramName)
196
197        return layerName, layerObj
198
199    def _getLayerParam(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        layers = {}
209        layerNames = self.getOwsParam(paramName)
210
211        # Select the first layer if several are requested.
212        # This plays nicer with mapClient.
213        #if ',' in layerName:
214        layerNames = layerNames.split(',')
215            #raise InvalidParameterValue(
216            #    'Multi-layer GetMap requests are not supported', 'layers')
217        for layerName in layerNames:
218            try:
219                layerObj = self.layers[layerName]
220                layers[layerName] = layerObj
221            except KeyError:
222                raise InvalidParameterValue('Layer %s not found' % layerName,
223                                        paramName)
224
225        #return layerName, layerObj
226        return layers
227
228    def _getFormatParam(self):
229        format = self.getOwsParam('format', default='image/png')
230        if format not in self._pilImageFormats:
231            raise InvalidParameterValue(
232                'Format %s not supported' % format, 'format')
233
234        return format
235
236    _escapedDimNames = ['width', 'height', 'version', 'request',
237                        'layers', 'styles', 'crs', 'srs', 'bbox',
238                        'format', 'transparent', 'bgcolor',
239                        'exceptions']
240
241    def _getDimValues(self, layerObj):
242        dimValues = {}
243        for dimName, dim in layerObj.dimensions.items():
244            defaultValue = dim.extent[0]
245            escapedDimName=self._mapDimToParam(dimName)
246            dimValues[escapedDimName] = self.getOwsParam(escapedDimName,
247                                                  default=defaultValue)
248        return dimValues
249
250    def _mapDimToParam(self, dimName):
251        """
252        Dimension names might clash with WMS parameter names, making
253        them inaccessible in WMS requests.  This method maps a
254        dimension name to a parameter name that appears in the
255        capabilities document and WMS requests.
256
257        """
258        if dimName.lower() in self._escapedDimNames:
259            return dimName+'_dim'
260        else:
261            return dimName
262       
263    def _mapParamToDim(self, dimParam):
264        """
265        Maps a dimension parameter name to it's real dimension name.
266
267        @see: _mapDimToParam()
268
269        """
270        try:
271            dimName = re.match(r'(.*)_dim$', dimParam).group(1)
272            if dimName.lower() in self._escapedDimNames:
273                return dimName
274            else:
275                return dimParam
276        except AttributeError:
277            return dimParam
278
279
280    def _retrieveSlab(self, layerObj, srs, dimValues, renderOpts):
281        # Find the slab in the cache first
282        cacheKey = layerObj.getCacheKey(srs, dimValues)
283        slab = self._layerSlabCache.get(cacheKey)
284        if slab is None:
285            slab = layerObj.getSlab(srs, dimValues, renderOpts)
286            if cacheKey is not None:
287                self._layerSlabCache[cacheKey] = slab
288
289        return slab
290
291    #-------------------------------------------------------------------------
292    # OWS Operation methods
293   
294    def GetMap(self):
295
296        # Housekeeping
297        version = self.getOwsParam('version', default=self.validVersions[0])
298        if version not in self.validVersions:
299            raise InvalidParameterValue('Version %s not supported' % version,
300                                        'version')
301        styles = self.getOwsParam('styles', default='')
302        transparent = self.getOwsParam('transparent', default='FALSE')
303        bgcolor = self.getOwsParam('bgcolor', default='0xFFFFFF')
304
305        # Layer handling
306        #layerName, layerObj = self._getLayerParam()
307        layers = self._getLayerParam()
308        log.debug('GetMap request for layer(s) %s'%layers)
309        # Coordinate parameters
310        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
311        width = int(self.getOwsParam('width'))
312        height = int(self.getOwsParam('height'))
313
314        if version == '1.1.1':
315            srs = self.getOwsParam('srs')
316        else:
317            srs = self.getOwsParam('crs')
318
319        #if srs not in layerObj.crss:
320         #   raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerName, srs))
321
322        # Get format
323        format = self.getOwsParam('format')
324        if format not in self._pilImageFormats:
325            raise InvalidParameterValue(
326                'Format %s not supported' % format, 'format')
327
328        finalImg = Image.new('RGBA', (width, height), (0,0,0,0))
329       
330        # Multiple Layers handling.. 
331        for layerName, layerObj in layers.iteritems():
332            if srs not in layerObj.crss:
333                raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerName, srs))
334
335            dimValues = self._getDimValues(layerObj)
336           
337            #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
338            restoredDimValues={}
339            for dim in dimValues:
340                restoredDim=self._mapParamToDim(dim)
341                restoredDimValues[restoredDim]=dimValues[dim]
342               
343            #-------------------------------------------------------
344            # The real work
345            #!TODO: Minimum and maximum values
346
347            slab = self._retrieveSlab(layerObj, srs, restoredDimValues,
348                                      dict(minValue=0, maxValue=100))
349
350            # We must request a bbox within the layer's bbox.
351            lbbox = layerObj.getBBox(srs)
352            ibbox = bbox_util.intersection(bbox, lbbox)
353
354            log.debug('bbox = %s' % (bbox,))
355            log.debug('lbbox = %s' % (lbbox,))
356            log.debug('ibbox = %s' % (ibbox,))
357
358            # If bbox is not within layerObj.bbox then we need to calculate the
359            # pixel offset of the inner bbox, request the right width/height
360            # and paste the image into a blank background
361            if bbox == ibbox:
362                img = slab.getImage(bbox, width, height)
363                log.debug('slab image.size = %s' % (img.size,))
364                       
365            else:
366               
367                ix0, iy0 = bbox_util.geoToPixel(ibbox[0], ibbox[3], bbox, width, height,
368                                                roundUpY=True)
369                ix1, iy1 = bbox_util.geoToPixel(ibbox[2], ibbox[1], bbox, width, height,
370                                                roundUpX=True)
371                iw = ix1-ix0
372                ih = iy1-iy0
373                log.debug('Deduced inner image: %s, (%d x %d)' % ((ix0, iy0, ix1, iy1), iw, ih))
374                img1 = slab.getImage(ibbox, iw, ih)
375
376                img = Image.new('RGBA', (width, height))
377                img.paste(img1, (ix0, iy0))
378               
379            finalImg = Image.composite(finalImg, img, finalImg) 
380           
381         
382         
383       
384        # IE < 7 doesn't display the alpha layer right.  Here we sniff the
385        # user agent and remove the alpha layer if necessary.
386        try:
387            ua = request.headers['User-Agent']
388        except:
389            pass
390        else:
391            if 'MSIE' in ua and 'MSIE 7' not in ua:
392                finalImg = finalImg.convert('RGB')
393
394        buf = StringIO()
395        finalImg.save(buf, self._pilImageFormats[format])
396
397        response.headers['Content-Type'] = format
398        response.write(buf.getvalue())
399
400
401    def GetContext(self):
402        """
403        Return a WebMap Context document for a given set of layers.
404
405        """
406        # Parameters
407        layers = self.getOwsParam('layers', default=None)
408        format = self.getOwsParam('format', default='text/xml')
409
410        # Filter self.layers for selected layers
411        if layers is not None:
412            newLayerMap = {}
413            for layerName in layers.split(','):
414                try:
415                    newLayerMap[layerName] = self.layers[layerName]
416                except KeyError:
417                    raise InvalidParameterValue('Layer %s not found' % layerName,
418                                                'layers')
419                   
420            self.layers = newLayerMap
421
422        # Automatically select the first bbox/crs for the first layer
423        aLayer = self.layers.values()[0]
424        crs = aLayer.crss[0]
425        bb = aLayer.getBBox(crs)
426        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
427
428        # Initialise as if doing GetCapabilities
429        ows_controller.initCapabilities()
430        self._loadCapabilities()
431
432        if format == 'text/xml':
433            response.headers['Content-Type'] = format
434            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
435            return t.generate(c=c).render()
436        elif format == 'application/json':
437            response.headers['Content-Type'] = format
438            t = ows_controller.templateLoader.load('wms_context_json.txt',
439                                                   cls=NewTextTemplate)
440            return t.generate(c=c).render()
441        else:
442            raise InvalidParameterValue('Format %s not supported' % format)
443
444    def GetFeatureInfo(self):
445        # Housekeeping
446        version = self.getOwsParam('version', default=self.validVersions[0])
447        if version not in self.validVersions:
448            raise InvalidParameterValue('Version %s not supported' % version,
449                                        'version')
450
451        # Coordinate parameters
452        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
453        width = int(self.getOwsParam('width'))
454        height = int(self.getOwsParam('height'))
455         
456        # Get pixel location
457        i = int(self.getOwsParam('i'))
458        j = int(self.getOwsParam('j'))
459
460        # Translate to geo-coordinates
461        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
462        #start preparing GetFeatureInfo response. Assumes "HTML" output format
463
464        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
465       
466       
467        layers = self._getLayerParam('query_layers')
468        #Adjusts response for multiple layers
469        if len(layers) > 1:
470            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
471 
472        htmlResponse = htmlResponse+"<ul>"
473       
474        format = self.getOwsParam('info_format', default='text/html')
475        for layerName, layerObj in layers.iteritems():
476            log.debug('Format: %s' % format)
477            log.debug('Title: %s' % layerObj.title)
478            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
479        if format not in layerObj.featureInfoFormats:
480            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
481
482        if version == '1.1.1':
483                srs = self.getOwsParam('srs')
484        else:
485            srs = self.getOwsParam('crs')
486
487        if srs not in layerObj.crss:
488            raise InvalidParameterValue('Layer %s does not support SRS %s' %
489                                        (layerName, srs))
490
491        # Dimension handling
492        dimValues = {}
493        for dimName, dim in layerObj.dimensions.items():
494            defaultValue = dim.extent[0]
495            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
496       
497        response.headers['Content-Type'] = format
498        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
499
500    def GetLegend(self):
501        """
502        Return an image of the legend.
503
504        """
505        # Parameters
506        layerName, layerObj = self._getLayerParamInfo()
507        format = self._getFormatParam()
508
509        # This hook alows extra arguments to be passed to the layer backend.  It
510        # is required for UKCP.
511        renderOpts = dict(request_params=self._owsParams)
512
513        img = layerObj.getLegendImage(renderOpts=renderOpts)
514
515        buf = StringIO()
516        img.save(buf, self._pilImageFormats[format])
517
518        response.headers['Content-Type'] = format
519        response.write(buf.getvalue())
520
521
522    def GetInfo(self):
523        from pprint import pformat
524        request.headers['Content-Type'] = 'text/ascii'
525        response.write('Some info about this service\n')
526        for layer in model.ukcip02.layers:
527            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
528
529           
Note: See TracBrowser for help on using the repository browser.