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

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

Added LICENSE file and a short banner at the top of each python file.
Use add_license.py to add a license to new files.

NOTE: also contains a couple of files missed at last commit.

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