source: TI05-delivery/ows_framework/branches/ows_framework-refactor/ows_common/ows_common/pylons/wms_controller.py @ 3806

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