source: qesdi/wms_ddc_vis/trunk/lib/wms_ddc_vis/controllers/coastwms.py @ 5978

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

Added some experimental caching to the coastline layer that will hopefully east the large overhead of drawing the rivers onto the map.

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