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

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

Reverted to expecting Genshi 0.4. This requires a different TextTemplate? style.

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