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

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

Experimental change to rendering code

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