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

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

New COWS distribution. See [4005] for description.

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