source: cows/branches/cows-vis/cows/pylons/wms_controller.py @ 5265

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/branches/cows-vis/cows/pylons/wms_controller.py@5265
Revision 5265, 19.8 KB checked in by domlowe, 11 years ago (diff)

adding separate cowsclient pylons app

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
26import Image
27from genshi.template import NewTextTemplate
28
29from cows.model.wms import WmsDatasetSummary, Dimension, DataURL
30from cows.model import PossibleValues, WGS84BoundingBox, BoundingBox, Contents
31from cows.pylons import ows_controller
32from cows.exceptions import *
33from cows import bbox_util
34
35class WMSController(ows_controller.OWSController):
36    """
37    Subclass this controller in a pylons application and set the layerMapper
38    class attribute to implement a WMS.
39
40    @cvar layerMapper: an cows.service.wms_iface.ILayerMapper object.
41
42    """
43    layerMapper = None
44    #layers = {}   
45    _pilImageFormats = {
46        'image/png': 'PNG',
47        'image/jpg': 'JPEG',
48        'image/gif': 'GIF',
49        'image/tiff': 'TIFF',
50        'text/html': 'HTML'
51        }
52    _layerSlabCache = {}
53
54    #-------------------------------------------------------------------------
55    # Attributes required by OWSController
56
57    service = 'WMS'
58    owsOperations = (ows_controller.OWSController.owsOperations +
59        ['GetMap', 'GetContext', 'GetLegend', 'GetFeatureInfo', 'GetInfo'])
60    validVersions = ['1.1.1', '1.3.0']
61
62    #-------------------------------------------------------------------------
63
64    def __before__(self, **kwargs):
65        """
66        This default implementation of __before__() will pass all routes
67        arguments to the layer mapper to retrieve a list of layers for
68        this WMS.
69
70        It will be called automatically by pylons before each action method.
71
72        @todo: The layer mapper needs to come from somewhere.
73
74        """
75        #self.updateSequence = "hello"
76        log.debug("loading layers")
77        #print self.layers
78        self.layers = self.layerMapper.map(**kwargs)
79
80    #-------------------------------------------------------------------------
81    # Methods implementing stubs in OWSController
82
83    def _renderCapabilities(self, version, format):
84        if format == 'application/json':
85            t = ows_controller.templateLoader.load('wms_capabilities_json.txt',
86                                                   cls=NewTextTemplate)
87        elif version == '1.1.1':
88            t = ows_controller.templateLoader.load('wms_capabilities_1_1_1.xml')
89        elif version == '1.3.0':
90            t = ows_controller.templateLoader.load('wms_capabilities_1_3_0.xml')
91        else:
92            # We should never get here!  The framework should raise an exception before now.
93            raise RuntimeError("Version %s not supported" % version)
94       
95        return t.generate(c=c).render()
96
97    def _loadCapabilities(self):
98        """
99        @note: Assumes self.layers has already been created by __before__().
100
101        """
102        #!TODO: Add json format to GetCapabilities operation
103
104        ows_controller.addOperation('GetMap', formats=self._pilImageFormats.keys())
105        ows_controller.addOperation('GetContext', formats=['text/xml', 'application/json'])
106        ows_controller.addOperation('GetLegend',
107                                    formats=['image/png','text/html'])
108        ows_controller.addOperation('GetInfo')
109       
110        featureInfoFormats = Set()
111
112        log.debug('Loading capabilities contents')
113        c.capabilities.contents = Contents()
114        for layerName, layer in self.layers.items():
115            log.debug('LayerName: %s' % layerName)
116            log.debug('Loading layer %s' % layerName)
117
118            wgs84BBox = WGS84BoundingBox(layer.wgs84BBox[:2],
119                                         layer.wgs84BBox[2:])
120            # Get CRS/BBOX pairs
121            bboxObjs = []
122            for crs in layer.crss:
123                bbox = layer.getBBox(crs)
124                bboxObjs.append(BoundingBox(bbox[:2], bbox[2:], crs=crs))
125            # Get dimensions
126            dims = {}
127            for dimName, dim in layer.dimensions.items():
128                dimParam = self._mapDimToParam(dimName)
129                dims[dimParam] = Dimension(valuesUnit=dim.units,
130                                          unitSymbol=dim.units,
131                                          possibleValues=
132                                            PossibleValues.fromAllowedValues(dim.extent))
133            # Does the layer implement GetFeatureInfo?
134            if layer.featureInfoFormats:
135                queryable = True
136                featureInfoFormats.union_update(layer.featureInfoFormats)
137            else:
138                queryable = False
139               
140            #URL to WCS - uses named route 'wcsroute'
141            #TODO: Allow for a WCS blacklist to opt out of providing dataurls for certain datasets?
142            #TODO: How to make this more configurable - what if WCS is not coupled with WMS?
143            try:
144                version='1.0.0' #wcs version
145                wcsbaseurl=url_for('wcsroute', fileoruri=c.fileoruri,qualified=True)+'?'
146                dataURLs=[DataURL(format='WCS:CoverageDescription', onlineResource='%sService=WCS&Request=DescribeCoverage&Coverage=%s&Version=%s'%(wcsbaseurl, layerName, version))]
147            except GenerationException:
148                log.info("dataURLs not populated: could not generate WCS url with url_for('wcsroute', filedoruri=%s,qualified=True)"%c.fileoruri)
149                dataURLs=[]
150            # Create the cows object
151            ds = WmsDatasetSummary(identifier=layerName,
152                                   titles=[layer.title],
153                                   CRSs=layer.crss,
154                                   wgs84BoundingBoxes=[wgs84BBox],
155                                   boundingBoxes=bboxObjs,
156                                   abstracts=[layer.abstract],
157                                   dimensions=dims,
158                                   queryable=queryable,
159                                   dataURLs=dataURLs)
160
161            # Stuff that should go in the capabilities tree eventually
162            ds.legendSize = layer.legendSize
163            ds.legendFormats = ['image/png']
164
165            c.capabilities.contents.datasetSummaries.append(ds)
166
167        # Add this operation here after we have found all formats
168        ows_controller.addOperation('GetFeatureInfo',
169                                    formats = list(featureInfoFormats))
170
171    def _getLayerParamInfo(self, paramName='layers'):
172        """
173        Retrieve the layers parameter enforcing the rule of only
174        selecting one layer.
175
176        @param paramName: Overrides the query string parameter name to
177            look for.  This is usefull for implementing GetFeatureInfo.
178
179        """
180        layerName = self.getOwsParam(paramName)
181
182        # Select the first layer if several are requested.
183        # This plays nicer with mapClient.
184        if ',' in layerName:
185            #layerName = layerName.split(',')[0]
186            raise InvalidParameterValue(
187                'Multi-layer GetLegend requests are not supported', 'layers')
188        try:
189            layerObj = self.layers[layerName]
190        except KeyError:
191            raise InvalidParameterValue('Layer %s not found' % layerName,
192                                        paramName)
193
194        return layerName, layerObj
195
196    def _getLayerParam(self, paramName='layers'):
197        """
198        Retrieve the layers parameter enforcing the rule of only
199        selecting one layer.
200
201        @param paramName: Overrides the query string parameter name to
202            look for.  This is usefull for implementing GetFeatureInfo.
203
204        """
205        layers = {}
206        layerNames = self.getOwsParam(paramName)
207
208        # Select the first layer if several are requested.
209        # This plays nicer with mapClient.
210        #if ',' in layerName:
211        layerNames = layerNames.split(',')
212            #raise InvalidParameterValue(
213            #    'Multi-layer GetMap requests are not supported', 'layers')
214        for layerName in layerNames:
215            try:
216                layerObj = self.layers[layerName]
217                layers[layerName] = layerObj
218            except KeyError:
219                raise InvalidParameterValue('Layer %s not found' % layerName,
220                                        paramName)
221
222        #return layerName, layerObj
223        return layers
224
225    def _getFormatParam(self):
226        format = self.getOwsParam('format', default='image/png')
227        if format not in self._pilImageFormats:
228            raise InvalidParameterValue(
229                'Format %s not supported' % format, 'format')
230        return format
231
232    _escapedDimNames = ['width', 'height', 'version', 'request',
233                        'layers', 'styles', 'crs', 'srs', 'bbox',
234                        'format', 'transparent', 'bgcolor',
235                        'exceptions']
236
237    def _getDimValues(self, layerObj):
238        dimValues = {}
239        for dimName, dim in layerObj.dimensions.items():
240            defaultValue = dim.extent[0]
241            escapedDimName=self._mapDimToParam(dimName)
242            dimValues[escapedDimName] = self.getOwsParam(escapedDimName,
243                                                  default=defaultValue)
244        return dimValues
245
246    def _mapDimToParam(self, dimName):
247        """
248        Dimension names might clash with WMS parameter names, making
249        them inaccessible in WMS requests.  This method maps a
250        dimension name to a parameter name that appears in the
251        capabilities document and WMS requests.
252
253        """
254        if dimName.lower() in self._escapedDimNames:
255            return dimName+'_dim'
256        else:
257            return dimName
258       
259    def _mapParamToDim(self, dimParam):
260        """
261        Maps a dimension parameter name to it's real dimension name.
262
263        @see: _mapDimToParam()
264
265        """
266        try:
267            dimName = re.match(r'(.*)_dim$', dimParam).group(1)
268            if dimName.lower() in self._escapedDimNames:
269                return dimName
270            else:
271                return dimParam
272        except AttributeError:
273            return dimParam
274
275
276    def _retrieveSlab(self, layerObj, srs, dimValues, renderOpts):
277        # Find the slab in the cache first
278        cacheKey = layerObj.getCacheKey(srs, dimValues)
279        slab = self._layerSlabCache.get(cacheKey)
280        if slab is None:
281            slab = layerObj.getSlab(srs, dimValues, renderOpts)
282            if cacheKey is not None:
283                self._layerSlabCache[cacheKey] = slab
284
285        return slab
286
287    #-------------------------------------------------------------------------
288    # OWS Operation methods
289   
290    def GetMap(self):
291
292        # Housekeeping
293        version = self.getOwsParam('version', default=self.validVersions[0])
294        if version not in self.validVersions:
295            raise InvalidParameterValue('Version %s not supported' % version,
296                                        'version')
297        styles = self.getOwsParam('styles', default='')
298        transparent = self.getOwsParam('transparent', default='FALSE')
299        bgcolor = self.getOwsParam('bgcolor', default='0xFFFFFF')
300
301        # Layer handling
302        #layerName, layerObj = self._getLayerParam()
303        layers = self._getLayerParam()
304        log.debug('GetMap request for layer(s) %s'%layers)
305        # Coordinate parameters
306        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
307        width = int(self.getOwsParam('width'))
308        height = int(self.getOwsParam('height'))
309
310        if version == '1.1.1':
311            srs = self.getOwsParam('srs')
312        else:
313            srs = self.getOwsParam('crs')
314
315        #if srs not in layerObj.crss:
316         #   raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerName, srs))
317
318        # Get format
319        format = self.getOwsParam('format')
320        if format not in self._pilImageFormats:
321            raise InvalidParameterValue(
322                'Format %s not supported' % format, 'format')
323
324        finalImg = Image.new('RGBA', (width, height), (0,0,0,0))
325       
326        # Multiple Layers handling.. 
327        for layerName, layerObj in layers.iteritems():
328            if srs not in layerObj.crss:
329                raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerName, srs))
330
331            dimValues = self._getDimValues(layerObj)
332           
333            #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
334            restoredDimValues={}
335            for dim in dimValues:
336                restoredDim=self._mapParamToDim(dim)
337                restoredDimValues[restoredDim]=dimValues[dim]
338               
339            #-------------------------------------------------------
340            # The real work
341            #!TODO: Minimum and maximum values
342
343            slab = self._retrieveSlab(layerObj, srs, restoredDimValues,
344                                      dict(minValue=0, maxValue=100))
345
346            # We must request a bbox within the layer's bbox.
347            lbbox = layerObj.getBBox(srs)
348            ibbox = bbox_util.intersection(bbox, lbbox)
349
350            log.debug('bbox = %s' % (bbox,))
351            log.debug('lbbox = %s' % (lbbox,))
352            log.debug('ibbox = %s' % (ibbox,))
353
354            # If bbox is not within layerObj.bbox then we need to calculate the
355            # pixel offset of the inner bbox, request the right width/height
356            # and paste the image into a blank background
357            if bbox == ibbox:
358                img = slab.getImage(bbox, width, height)
359                log.debug('slab image.size = %s' % (img.size,))
360                       
361            else:
362               
363                ix0, iy0 = bbox_util.geoToPixel(ibbox[0], ibbox[3], bbox, width, height,
364                                                roundUpY=True)
365                ix1, iy1 = bbox_util.geoToPixel(ibbox[2], ibbox[1], bbox, width, height,
366                                                roundUpX=True)
367                iw = ix1-ix0
368                ih = iy1-iy0
369                log.debug('Deduced inner image: %s, (%d x %d)' % ((ix0, iy0, ix1, iy1), iw, ih))
370                img1 = slab.getImage(ibbox, iw, ih)
371
372                img = Image.new('RGBA', (width, height))
373                img.paste(img1, (ix0, iy0))
374               
375            finalImg = Image.composite(finalImg, img, finalImg) 
376           
377         
378         
379       
380        # IE < 7 doesn't display the alpha layer right.  Here we sniff the
381        # user agent and remove the alpha layer if necessary.
382        try:
383            ua = request.headers['User-Agent']
384        except:
385            pass
386        else:
387            if 'MSIE' in ua and 'MSIE 7' not in ua:
388                finalImg = finalImg.convert('RGB')
389
390        buf = StringIO()
391        finalImg.save(buf, self._pilImageFormats[format])
392
393        response.headers['Content-Type'] = format
394        response.write(buf.getvalue())
395
396
397    def GetContext(self):
398        """
399        Return a WebMap Context document for a given set of layers.
400
401        """
402        # Parameters
403        layers = self.getOwsParam('layers', default=None)
404        format = self.getOwsParam('format', default='text/xml')
405
406        # Filter self.layers for selected layers
407        if layers is not None:
408            newLayerMap = {}
409            for layerName in layers.split(','):
410                try:
411                    newLayerMap[layerName] = self.layers[layerName]
412                except KeyError:
413                    raise InvalidParameterValue('Layer %s not found' % layerName,
414                                                'layers')
415                   
416            self.layers = newLayerMap
417
418        # Automatically select the first bbox/crs for the first layer
419        aLayer = self.layers.values()[0]
420        crs = aLayer.crss[0]
421        bb = aLayer.getBBox(crs)
422        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
423
424        # Initialise as if doing GetCapabilities
425        ows_controller.initCapabilities()
426        self._loadCapabilities()
427
428        if format == 'text/xml':
429            response.headers['Content-Type'] = format
430            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
431            return t.generate(c=c).render()
432        elif format == 'application/json':
433            response.headers['Content-Type'] = format
434            t = ows_controller.templateLoader.load('wms_context_json.txt',
435                                                   cls=NewTextTemplate)
436            return t.generate(c=c).render()
437        else:
438            raise InvalidParameterValue('Format %s not supported' % format)
439
440    def GetFeatureInfo(self):
441        # Housekeeping
442        version = self.getOwsParam('version', default=self.validVersions[0])
443        if version not in self.validVersions:
444            raise InvalidParameterValue('Version %s not supported' % version,
445                                        'version')
446
447        # Coordinate parameters
448        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
449        width = int(self.getOwsParam('width'))
450        height = int(self.getOwsParam('height'))
451         
452        # Get pixel location
453        i = int(self.getOwsParam('i'))
454        j = int(self.getOwsParam('j'))
455
456        # Translate to geo-coordinates
457        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
458        #start preparing GetFeatureInfo response. Assumes "HTML" output format
459
460        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
461       
462       
463        layers = self._getLayerParam('query_layers')
464        #Adjusts response for multiple layers
465        if len(layers) > 1:
466            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
467 
468        htmlResponse = htmlResponse+"<ul>"
469       
470        format = self.getOwsParam('info_format', default='text/html')
471        for layerName, layerObj in layers.iteritems():
472            log.debug('Format: %s' % format)
473            log.debug('Title: %s' % layerObj.title)
474            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
475        if format not in layerObj.featureInfoFormats:
476            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
477
478        if version == '1.1.1':
479                srs = self.getOwsParam('srs')
480        else:
481            srs = self.getOwsParam('crs')
482
483        if srs not in layerObj.crss:
484            raise InvalidParameterValue('Layer %s does not support SRS %s' %
485                                        (layerName, srs))
486
487        # Dimension handling
488        dimValues = {}
489        for dimName, dim in layerObj.dimensions.items():
490            defaultValue = dim.extent[0]
491            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
492       
493        response.headers['Content-Type'] = format
494        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
495
496    def GetLegend(self):
497        """
498        Return an image of the legend.
499
500        """
501        # Parameters
502        layerName, layerObj = self._getLayerParamInfo()
503        format = self._getFormatParam()
504
505        # This hook alows extra arguments to be passed to the layer backend.  It
506        # is required for UKCP.
507        renderOpts = dict(request_params=self._owsParams)
508
509        img = layerObj.getLegendImage(renderOpts=renderOpts)
510
511        buf = StringIO()
512        img.save(buf, self._pilImageFormats[format])
513
514        response.headers['Content-Type'] = format
515        response.write(buf.getvalue())
516
517
518    def GetInfo(self):
519        from pprint import pformat
520        request.headers['Content-Type'] = 'text/ascii'
521        response.write('Some info about this service\n')
522        for layer in model.ukcip02.layers:
523            response.write('Layer %s: %s\n' % (layer, pformat(g.ukcip02_layers[layer].__dict__)))
524
525           
Note: See TracBrowser for help on using the repository browser.