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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/trunk/cows/pylons/wms_controller.py@5122
Revision 5122, 18.7 KB checked in by domlowe, 13 years ago (diff)

removing 'return request' which wierdly has just showed up as a bug now.

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