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

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

WCS urls now generated in WebMapContext? documents

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