source: cowsserver/trunk/lib/cowsserver/controllers/coastwms.py @ 6145

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cowsserver/trunk/lib/cowsserver/controllers/coastwms.py@6145
Revision 6145, 16.3 KB checked in by pnorton, 10 years ago (diff)

Cleaned up the coastwms controller and modified the owslib tests.

Line 
1import logging
2
3
4from StringIO import StringIO
5from sets import Set
6import genshi.template 
7from cows.pylons import ows_controller
8from pylons import config
9from cows.service.imps.data_reader_geoplot_backend.rendering_option import RenderingOption
10from cows.service.imps.data_reader_geoplot_backend.slab_options_parser import SlabOptionsParser
11from cows.service.imps.data_reader_geoplot_backend.slab_options_json_generator import SlabJSONGenerator
12from cows.model import WGS84BoundingBox, BoundingBox, Contents
13from cows.exceptions import InvalidParameterValue
14
15from routes import url_for
16from cows.model.wms import WmsDatasetSummary
17from cowsserver.lib.wms_request_log_utils import buildLogString, wms_request_logger
18import cowsserver.lib.image_utils as image_utils
19
20import time
21import numpy
22from paste.deploy.converters import asbool
23
24import pylons
25
26from geoplot.layer_drawer_coastline import LayerDrawerCoastlines
27import matplotlib.colors
28
29from cowsserver.lib.image_import import Image
30from cowsserver.lib.base import request, response, c
31
32from cows.model.wms import MetadataURL
33from cows.xml.iso19115_subset import OnlineResource
34
35from cowsserver.lib.modified_beaker_cache_decorator import beaker_cache
36
37log = logging.getLogger(__name__)
38
39class CoastwmsController(ows_controller.OWSController):
40
41    coastlineRenderingOptions = [
42        RenderingOption('coastline_colour', "Coastline Colour" ,str , 'black'),
43        RenderingOption('coastline_width', "Coastline Width" ,float , 0.5 ),
44        RenderingOption('resolution', "Coastline Resolution" ,str , "auto", ["coarse","low", "intermediate", "high", "auto"]  ),           
45    ]
46   
47    riverRenderingOptions = [
48        RenderingOption('resolution', "River Resolution" ,str , "auto", ["coarse","low", "intermediate", "high", "auto"]  ),                                                             
49        RenderingOption('river_colour', "River Colour" ,str , 'blue'), 
50    ]
51   
52    landMassRenderingOptions = [
53        RenderingOption('resolution', "Coastline Resolution" ,str , "auto", ["coarse","low", "intermediate", "high", "auto"]  ),                               
54        RenderingOption('land_colour', "Land Colour" ,str , 'green'),                               
55    ]
56       
57
58    #layers = {}   
59    _pilImageFormats = {
60        'image/png': 'PNG',
61        'image/jpg': 'JPEG',
62        'image/gif': 'GIF',
63        'image/tiff': 'TIFF'
64        }
65   
66    _layerSlabCache = {}
67
68    #-------------------------------------------------------------------------
69    # Attributes required by OWSController
70
71    service = 'WMS'
72    owsOperations = (ows_controller.OWSController.owsOperations +
73        ['GetMap', 'GetContext', 'GetLegend', 'GetFeatureInfo', 'GetInfo', 'GetDisplayOptions'])
74   
75    validVersions = ['1.1.1', '1.3.0']
76
77
78    def __before__(self, **kwargs):
79        wms_request_logger.info( buildLogString(request) )
80
81    def _renderCapabilities(self, version, format):
82        if format == 'application/json':
83            t = ows_controller.templateLoader.load('wms_capabilities_json.txt',
84                                                   cls=genshi.template.NewTextTemplate)
85        elif version == '1.1.1':
86            t = ows_controller.templateLoader.load('wms_capabilities_1_1_1.xml')
87        elif version == '1.3.0':
88            t = ows_controller.templateLoader.load('wms_capabilities_1_3_0.xml')
89        else:
90            # We should never get here!  The framework should raise an exception before now.
91            raise RuntimeError("Version %s not supported" % version)
92       
93        return t.generate(c=c).render()
94
95    def _loadCapabilities(self):
96        """
97        @note: Assumes self.layers has already been created by __before__().
98
99        """
100        #!TODO: Add json format to GetCapabilities operation
101
102        ows_controller.addOperation('GetMap', formats=self._pilImageFormats.keys())
103        ows_controller.addOperation('GetContext', formats=['text/xml', 'application/json'])
104
105       
106        featureInfoFormats = Set()
107
108        log.debug('Loading capabilities contents')
109        c.capabilities.contents = Contents()
110       
111        layers = ( ('coastline', 'coastline', 'Coast Outline'), 
112                   ('rivers', 'rivers', 'Rivers'),
113                   ('landmass', 'landmass', 'Land Mass'),)
114       
115       
116        for layerName, title,  abstract in layers:
117            log.debug('LayerName: %s' % layerName)
118            log.debug('Loading layer %s' % layerName)
119
120            wgs84BBox = WGS84BoundingBox((-180,-90), (180,90))
121
122            # Get CRS/BBOX pairs
123            bboxObjs = []
124            for crs in ('EPSG:4326', 'CRS:84', 'WGS84'):
125                bbox = [-180,-90,180,90]
126                bboxObjs.append(BoundingBox(bbox[:2], bbox[2:], crs=crs))
127               
128
129               
130            #URL to WCS - uses named route 'wcsroute'
131            #TODO: Allow for a WCS blacklist to opt out of providing dataurls for certain datasets?
132            #TODO: How to make this more configurable - what if WCS is not coupled with WMS?
133#            try:
134#                version='1.0.0' #wcs version
135#                wcsbaseurl=url_for('wcsroute', fileoruri=c.fileoruri,qualified=True)+'?'
136#                dataURLs=[DataURL(format='WCS:CoverageDescription', onlineResource='%sService=WCS&Request=DescribeCoverage&Coverage=%s&Version=%s'%(wcsbaseurl, layerName, version))]
137#            except GenerationException:
138#                log.info("dataURLs not populated: could not generate WCS url with url_for('wcsroute', filedoruri=%s,qualified=True)"%c.fileoruri)
139#                dataURLs=[]
140           
141            onlineRes = OnlineResource(self._getIndexActionURL() +\
142                                    "?request=GetDisplayOptions&layers=%s" % layerName)
143            metadataURL = MetadataURL(metadataType='display_options', format='application/json', onlineResource=onlineRes)
144       
145            # Create the cows object
146            ds = WmsDatasetSummary(identifier=layerName,
147                                   titles=[title],
148                                   CRSs=('EPSG:4326', 'CRS:84', 'WGS84'),
149                                   wgs84BoundingBoxes=[wgs84BBox],
150                                   boundingBoxes=bboxObjs,
151                                   abstracts=[abstract],
152                                   dimensions={},
153                                   queryable=False,
154                                   dataURLs=[],
155                                   styles=[],
156                                   metadataURLs=[metadataURL])
157
158            # Stuff that should go in the capabilities tree eventually
159            ds.legendSize = (630,80)
160            ds.legendFormats = ['image/png']
161
162            c.capabilities.contents.datasetSummaries.append(ds)
163       
164        c.capabilities.serviceIdentification.titles=['coastline']
165       
166        # Add this operation here after we have found all formats
167        ows_controller.addOperation('GetFeatureInfo',
168                                    formats = list(featureInfoFormats))
169
170
171    _escapedDimNames = ['width', 'height', 'version', 'request',
172                        'layers', 'styles', 'crs', 'srs', 'bbox',
173                        'format', 'transparent', 'bgcolor',
174                        'exceptions']
175
176
177    def _mapDimToParam(self, dimName):
178        """
179        Dimension names might clash with WMS parameter names, making
180        them inaccessible in WMS requests.  This method maps a
181        dimension name to a parameter name that appears in the
182        capabilities document and WMS requests.
183
184        """
185        if dimName.lower() in self._escapedDimNames:
186            return dimName+'_dim'
187        else:
188            return dimName
189       
190    #@beaker_cache()
191    def GetMap(self):       
192        startTime = time.time()
193
194        # Layer handling
195        layerName = self.getOwsParam('layers')
196       
197        if layerName == 'rivers':
198            renderingOpts = CoastwmsController.riverRenderingOptions
199        elif layerName == 'landmass':
200            renderingOpts = CoastwmsController.landMassRenderingOptions
201        else:
202            renderingOpts = CoastwmsController.coastlineRenderingOptions
203           
204        parser = SlabOptionsParser(renderingOpts, request.params)
205       
206        # Housekeeping
207        version = self.getOwsParam('version', default=self.validVersions[0])
208        if version not in self.validVersions:
209            raise InvalidParameterValue('Version %s not supported' % version,
210                                        'version')
211        bgcolor = self.getOwsParam('bgcolor', default='0xFFFFFF')
212        transparent = self.getOwsParam('transparent', default='FALSE').lower() == 'true'
213       
214        # Coordinate parameters
215        bbox = tuple(float(x) for x in self.getOwsParam('bbox').split(','))
216        width = int(self.getOwsParam('width'))
217        height = int(self.getOwsParam('height'))
218
219        # Get format
220        format = self.getOwsParam('format')
221        if format not in self._pilImageFormats:
222            raise InvalidParameterValue(
223                'Format %s not supported' % format, 'format')
224
225        longResolution = parser.getOption('resolution')
226        resMap = {'coarse':'c','low':'l','intermediate':'i','high':'h', 'full':'f', 'auto':None}
227        resolution = resMap[longResolution]
228       
229        log.debug("bgcolor = %s" % (bgcolor,))
230       
231        # always set transparent to true then apply
232        # the background colour later
233        ldgArgs = {'transparent':True, 'resolution':resolution}
234       
235        convertColour = None
236       
237        # as matplotlib will make the image all one colour
238        # and use the alpha channel to draw the lines, to aid in caching we can
239        # request the image in black and then convert the colour afterwards (so
240        # only the black coloured image is cached.)
241       
242        if layerName  == 'rivers':
243            convertColour = self._colourToRGB(parser.getOption('river_colour'), 'blue')
244            ldgArgs['riverColour'] = 'black'
245            ldgArgs['coastlineColour'] = None
246           
247        elif layerName == 'landmass':
248            convertColour = self._colourToRGB(parser.getOption('land_colour'), 'green')
249            ldgArgs['landColour'] = 'black'
250            ldgArgs['coastlineColour'] = None
251        else:
252            convertColour = self._colourToRGB(parser.getOption('coastline_colour'), 'black')
253            ldgArgs['coastlineColour'] = 'black'
254            ldgArgs['coastlineWidth']  = parser.getOption('coastline_width')
255           
256        xLimits = (bbox[0], bbox[2])
257        yLimits = (bbox[1], bbox[3])
258       
259        finalImg = self._getLDCImage(ldgArgs, xLimits, yLimits, width, height)
260       
261       
262        if convertColour != None:
263           
264            log.debug("convertColour = %s" % (convertColour,))
265            st = time.time()
266           
267            finalImg = image_utils.setImageRGB(finalImg, convertColour)
268
269            log.debug("converted in %ss" % (time.time() - st,))
270       
271        # at this point finalImg has a transparent background, if transparent is
272        # false then apply a background colour. This avoids the need for caching
273        # all the different background colours and this process of filling in the
274        # background is pretty quick.
275        if not transparent:
276           
277            st = time.time()
278            backgroundColour = self._colourToRGB(bgcolor, (255,255,255))
279            backgroundColour = backgroundColour + (255,) # add the alpha
280            finalImg = image_utils.setBGColour(finalImg, backgroundColour)
281            log.debug("set background in %ss" % (time.time() - st,))
282       
283        # IE < 7 doesn't display the alpha layer right.  Here we sniff the
284        # user agent and remove the alpha layer if necessary.
285        try:
286            ua = request.headers['User-Agent']
287            #log.debug("ua = %s" % (ua,))
288        except:
289            pass
290        else:
291            if 'MSIE 6.0' in ua:
292                finalImg = finalImg.convert('RGB')
293
294        buf = StringIO()
295        finalImg.save(buf, self._pilImageFormats[format])
296
297        response.headers['Content-Type'] = format
298        response.write(buf.getvalue())
299       
300        log.debug("got coastline in %s" % (time.time() - startTime,))
301
302    def GetContext(self):
303        """
304        Return a WebMap Context document for a given set of layers.
305
306        """
307        # Parameters
308        layers = self.getOwsParam('layers', default=None)
309        format = self.getOwsParam('format', default='text/xml')
310
311        # Filter self.layers for selected layers
312        if layers is not None:
313            newLayerMap = {}
314            for layerName in layers.split(','):
315                try:
316                    newLayerMap[layerName] = self.layers[layerName]
317                except KeyError:
318                    raise InvalidParameterValue('Layer %s not found' % layerName,
319                                                'layers')
320                   
321            self.layers = newLayerMap
322
323        # Automatically select the first bbox/crs for the first layer
324        aLayer = self.layers.values()[0]
325        crs = aLayer.crss[0]
326        bb = aLayer.getBBox(crs)
327        c.bbox = BoundingBox(bb[:2], bb[2:], crs)
328
329        # Initialise as if doing GetCapabilities
330        ows_controller.initCapabilities()
331        self._loadCapabilities()
332
333        if format == 'text/xml':
334           
335            response.headers['Content-Type'] = format
336            t = ows_controller.templateLoader.load('wms_context_1_1_1.xml')
337            return t.generate(c=c).render()
338       
339        elif format == 'application/json':
340           
341            response.headers['Content-Type'] = format
342            t = ows_controller.templateLoader.load('wms_context_json.txt',
343                                                   cls=genshi.template.NewTextTemplate)
344            return t.generate(c=c).render()
345       
346        else:
347            raise InvalidParameterValue('Format %s not supported' % format)
348
349    def GetDisplayOptions(self):
350       
351        layer = self.getOwsParam('layers', default=None)
352       
353       
354        log.debug("layer = %s" % (layer,))
355        if layer == 'rivers':
356            generator = SlabJSONGenerator({'':CoastwmsController.riverRenderingOptions})
357        elif layer == 'landmass':
358            generator = SlabJSONGenerator({'':CoastwmsController.landMassRenderingOptions})
359        else:
360            generator = SlabJSONGenerator({'':CoastwmsController.coastlineRenderingOptions})
361           
362        request.headers['Content-Type'] = 'application/json'
363        response.write( generator.generateJSON() )
364
365    def _colourToRGB(self, colour, default=None, scaleTo255=True):
366       
367        if colour == None:
368            return default
369       
370        try:
371            if colour.find('0x') == 0:
372                colour = '#' + colour[2:]
373               
374            convertColour = matplotlib.colors.colorConverter.to_rgb(colour)
375           
376            if scaleTo255:
377                convertColour = tuple([int(round(y*255)) for y in convertColour])
378           
379        except:
380            log.warning("Error converting colour %s to rgb value." % (colour,))
381            return default
382        else:
383            return convertColour
384
385    def _getLDCImage(self, ldgArgs, xLimits, yLimits, width, height, expire='never'):
386        dic = ldgArgs.copy()
387        dic['xLimits'] = xLimits
388        dic['yLimits'] = yLimits
389        dic['width'] = width
390        dic['height'] = height
391       
392        cache_key = repr(dic)
393
394        def create_func():
395            log.debug("generating, cache_key = %s" % (cache_key,))
396            log.debug("ldgArgs = %s" % (ldgArgs,))
397            ldg = LayerDrawerCoastlines(**ldgArgs)
398            return ldg.makeImage(xLimits, yLimits, width, height)
399   
400        enabled = pylons.config.get("cache_enabled", "True")
401
402        if not asbool(enabled):
403            log.debug("Caching disabled, skipping cache lookup")
404            return create_func()
405       
406        my_cache = pylons.cache.get_cache('LayerDrawerCoastlineCache', type='dbm')
407       
408        if expire == "never":
409            cache_expire = None
410        else:
411            cache_expire = expire
412       
413        img = my_cache.get_value(cache_key, createfunc=create_func, 
414                                      expiretime=cache_expire,)
415     
416        return img
417
418    def _getIndexActionURL(self):
419        """
420        Uses the pylons config to build a url for the index action of this contoller.
421        """
422               
423        indexURL = url_for(qualified=True, action='index')
424        return indexURL   
Note: See TracBrowser for help on using the repository browser.