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

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

Changed print statements to loggging calls where appropriate.

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        log.debug("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            log.debug('LayerName: %s' % 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        layers = {}
183        layerNames = self.getOwsParam(paramName)
184
185        # Select the first layer if several are requested.
186        # This plays nicer with mapClient.
187        #if ',' in layerName:
188        layerNames = layerNames.split(',')
189            #raise InvalidParameterValue(
190            #    'Multi-layer GetMap requests are not supported', 'layers')
191        for layerName in layerNames:
192            try:
193                layerObj = self.layers[layerName]
194                layers[layerName] = layerObj
195            except KeyError:
196                raise InvalidParameterValue('Layer %s not found' % layerName,
197                                        paramName)
198
199        #return layerName, layerObj
200        return layers
201
202    def _getFormatParam(self):
203        format = self.getOwsParam('format', default='image/png')
204        if format not in self._pilImageFormats:
205            raise InvalidParameterValue(
206                'Format %s not supported' % format, 'format')
207
208        return format
209
210    _escapedDimNames = ['width', 'height', 'version', 'request',
211                        'layers', 'styles', 'crs', 'srs', 'bbox',
212                        'format', 'transparent', 'bgcolor',
213                        'exceptions']
214
215    def _getDimValues(self, layerObj):
216        dimValues = {}
217        for dimName, dim in layerObj.dimensions.items():
218            defaultValue = dim.extent[0]
219            escapedDimName=self._mapDimToParam(dimName)
220            dimValues[escapedDimName] = self.getOwsParam(escapedDimName,
221                                                  default=defaultValue)
222        return dimValues
223
224    def _mapDimToParam(self, dimName):
225        """
226        Dimension names might clash with WMS parameter names, making
227        them inaccessible in WMS requests.  This method maps a
228        dimension name to a parameter name that appears in the
229        capabilities document and WMS requests.
230
231        """
232        if dimName.lower() in self._escapedDimNames:
233            return dimName+'_dim'
234        else:
235            return dimName
236       
237    def _mapParamToDim(self, dimParam):
238        """
239        Maps a dimension parameter name to it's real dimension name.
240
241        @see: _mapDimToParam()
242
243        """
244        try:
245            dimName = re.match(r'(.*)_dim$', dimParam).group(1)
246            if dimName.lower() in self._escapedDimNames:
247                return dimName
248            else:
249                return dimParam
250        except AttributeError:
251            return dimParam
252
253
254    def _retrieveSlab(self, layerObj, srs, dimValues, renderOpts):
255        # Find the slab in the cache first
256        cacheKey = layerObj.getCacheKey(srs, dimValues)
257       
258        slab = self._layerSlabCache.get(cacheKey)
259        if slab is None:
260            slab = layerObj.getSlab(srs, dimValues, renderOpts)
261            if cacheKey is not None:
262                self._layerSlabCache[cacheKey] = slab
263
264        return slab
265
266    #-------------------------------------------------------------------------
267    # OWS Operation methods
268   
269    def GetMap(self):
270
271        # Housekeeping
272        version = self.getOwsParam('version', default=self.validVersions[0])
273        if version not in self.validVersions:
274            raise InvalidParameterValue('Version %s not supported' % version,
275                                        'version')
276        styles = self.getOwsParam('styles', default='')
277        transparent = self.getOwsParam('transparent', default='FALSE')
278        bgcolor = self.getOwsParam('bgcolor', default='0xFFFFFF')
279
280        # Layer handling
281        #layerName, layerObj = self._getLayerParam()
282        layers = self._getLayerParam()
283        # Coordinate parameters
284        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
285        width = int(self.getOwsParam('width'))
286        height = int(self.getOwsParam('height'))
287
288        if version == '1.1.1':
289            srs = self.getOwsParam('srs')
290        else:
291            srs = self.getOwsParam('crs')
292
293        #if srs not in layerObj.crss:
294         #   raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerName, srs))
295
296        # Get format
297        format = self.getOwsParam('format')
298        if format not in self._pilImageFormats:
299            raise InvalidParameterValue(
300                'Format %s not supported' % format, 'format')
301
302        finalImg = Image.new('RGBA', (width, height), (0,0,0,0))
303       
304        # Multiple Layers handling.. 
305        for layerName, layerObj in layers.iteritems():
306            if srs not in layerObj.crss:
307                raise InvalidParameterValue('Layer %s does not support SRS %s' % (layerName, srs))
308
309            dimValues = self._getDimValues(layerObj)
310           
311            #now need to revert modified dim values (e.g. height_dim) back to dim values the layerMapper understands (e.g. height)
312            restoredDimValues={}
313            for dim in dimValues:
314                restoredDim=self._mapParamToDim(dim)
315                restoredDimValues[restoredDim]=dimValues[dim]
316               
317            #-------------------------------------------------------
318            # The real work
319            #!TODO: Minimum and maximum values
320
321            slab = self._retrieveSlab(layerObj, srs, restoredDimValues,
322                                      dict(minValue=0, maxValue=100))
323
324            # We must request a bbox within the layer's bbox.
325            lbbox = layerObj.getBBox(srs)
326            ibbox = bbox_util.intersection(bbox, lbbox)
327
328            log.debug('bbox = %s' % (bbox,))
329            log.debug('lbbox = %s' % (lbbox,))
330            log.debug('ibbox = %s' % (ibbox,))
331
332            # If bbox is not within layerObj.bbox then we need to calculate the
333            # pixel offset of the inner bbox, request the right width/height
334            # and paste the image into a blank background
335            if bbox == ibbox:
336                img = slab.getImage(bbox, width, height)
337                log.debug('slab image.size = %s' % (img.size,))
338                       
339            else:
340               
341                ix0, iy0 = bbox_util.geoToPixel(ibbox[0], ibbox[3], bbox, width, height,
342                                                roundUpY=True)
343                ix1, iy1 = bbox_util.geoToPixel(ibbox[2], ibbox[1], bbox, width, height,
344                                                roundUpX=True)
345                iw = ix1-ix0
346                ih = iy1-iy0
347                log.debug('Deduced inner image: %s, (%d x %d)' % ((ix0, iy0, ix1, iy1), iw, ih))
348                img1 = slab.getImage(ibbox, iw, ih)
349
350                img = Image.new('RGBA', (width, height))
351                img.paste(img1, (ix0, iy0))
352               
353            finalImg = Image.composite(finalImg, img, finalImg) 
354           
355         
356         
357       
358        # IE < 7 doesn't display the alpha layer right.  Here we sniff the
359        # user agent and remove the alpha layer if necessary.
360        try:
361            ua = request.headers['User-Agent']
362        except:
363            pass
364        else:
365            if 'MSIE' in ua and 'MSIE 7' not in ua:
366                finalImg = finalImg.convert('RGB')
367
368        buf = StringIO()
369        finalImg.save(buf, self._pilImageFormats[format])
370
371        response.headers['Content-Type'] = format
372        response.write(buf.getvalue())
373
374        return request
375
376    def GetContext(self):
377        """
378        Return a WebMap Context document for a given set of layers.
379
380        """
381        # Parameters
382        layers = self.getOwsParam('layers', default=None)
383
384        # Filter self.layers for selected layers
385        if layers is not None:
386            newLayerMap = {}
387            for layerName in layers.split(','):
388                try:
389                    newLayerMap[layerName] = self.layers[layerName]
390                except KeyError:
391                    raise InvalidParameterValue('Layer %s not found' % layerName,
392                                                'layers')
393                   
394            self.layers = newLayerMap
395
396        # Automatically select the first bbox/crs for the first layer
397        aLayer = self.layers.values()[0]
398        crs = aLayer.crss[0]
399        bb = aLayer.getBBox(crs)
400        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
401
402        # Initialise as if doing GetCapabilities
403        ows_controller.initCapabilities()
404        self._loadCapabilities()
405
406        response.headers['Content-Type'] = 'text/xml'
407        t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
408        return t.generate(c=c).render()
409
410    def GetFeatureInfo(self):
411        # Housekeeping
412        version = self.getOwsParam('version', default=self.validVersions[0])
413        if version not in self.validVersions:
414            raise InvalidParameterValue('Version %s not supported' % version,
415                                        'version')
416
417        # Coordinate parameters
418        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
419        width = int(self.getOwsParam('width'))
420        height = int(self.getOwsParam('height'))
421         
422        # Get pixel location
423        i = int(self.getOwsParam('i'))
424        j = int(self.getOwsParam('j'))
425
426        # Translate to geo-coordinates
427        x, y = bbox_util.pixelToGeo(i, j, bbox, width, height)
428        #start preparing GetFeatureInfo response. Assumes "HTML" output format
429
430        htmlResponse = "<html><body><p> <b>Feature Information about pixel position: "+self.getOwsParam('i')+","+self.getOwsParam('j')+"/geo position: "+str(x)+","+str(y) +"<b/></p>"
431       
432       
433        layers = self._getLayerParam('query_layers')
434        #Adjusts response for multiple layers
435        if len(layers) > 1:
436            htmlResponse = htmlResponse+" Multiple possible features found as follows:"
437 
438        htmlResponse = htmlResponse+"<ul>"
439       
440        format = self.getOwsParam('info_format', default='text/html')
441        for layerName, layerObj in layers.iteritems():
442            log.debug('Format: %s' % format)
443            log.debug('Title: %s' % layerObj.title)
444            log.debug('FeatureInfoFormats: %s' % layerObj.featureInfoFormats)
445        if format not in layerObj.featureInfoFormats:
446            raise InvalidParameterValue('Layer %s does not support GetFeatureInfo in format %s' %(layerName, format), 'info_format')
447
448        if version == '1.1.1':
449                srs = self.getOwsParam('srs')
450        else:
451            srs = self.getOwsParam('crs')
452
453        if srs not in layerObj.crss:
454            raise InvalidParameterValue('Layer %s does not support SRS %s' %
455                                        (layerName, srs))
456
457        # Dimension handling
458        dimValues = {}
459        for dimName, dim in layerObj.dimensions.items():
460            defaultValue = dim.extent[0]
461            dimValues[dimName] = self.getOwsParam(dimName, default=defaultValue)
462       
463        response.headers['Content-Type'] = format
464        response.write(layerObj.getFeatureInfo(format, srs, (x, y), dimValues))
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.