source: cows/trunk/cows/service/imps/csmlbackend/wms_csmllayer.py @ 5366

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cows/trunk/cows/service/imps/csmlbackend/wms_csmllayer.py@5366
Revision 5366, 18.4 KB checked in by domlowe, 11 years ago (diff)

Adding config option for legend font.

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"""
9implementation of ILayerMapper, IwmsLayer, IwmsDimension, ILayerSlab interfaces, as defined in wms_iface.py & wxs_iface.py
10
11"""
12import os, string
13import csml
14try:
15    import cdms2 as cdms
16except:
17    import cdms
18from cows.service.imps.csmlbackend.config import config
19import Image
20from copy import copy
21from cows.service.imps.pywms.render_imp import RGBARenderer
22from matplotlib import cm
23import cdtime
24import logging
25log = logging.getLogger(__name__)
26import numpy
27from MA import array
28import Image, ImageFont, ImageDraw
29from cows.service.wms_iface import IwmsLayer, IwmsDimension, IwmsLayerSlab
30from cows.service.imps.csmlbackend.csmlcommon import CSMLLayerMapper, CSMLConnector
31
32
33class CSMLwmsLayerMapper(CSMLLayerMapper):
34    """
35    Map keyword arguments to a collection of layers.
36    Supports the retrieval of sets of layers according to arbitrary
37    keyword/value pairs.
38    Implements  ILayerMapper
39   
40    """
41    def __init__(self):
42        super(CSMLwmsLayerMapper, self).__init__()
43   
44    #!TODO: (spascoe) Could be _getInfo() to make it clear it's internal
45    def getInfo(self, feature):
46        ''' given a csml feature, return info about the layer/feature
47        @return:   title, abstract, dimensions, units, crss '''
48
49        title, abstract = super(CSMLwmsLayerMapper, self).getInfo(feature)
50        units=feature.getDomainUnits() 
51        dimensions={}
52        tmpunits=copy(units)
53        tmpunits.reverse()
54        domain = feature.getDomain()
55        for dim in feature.getAxisLabels():
56            nextdim=CSMLwmsDimension(domain, dim, tmpunits.pop())
57            if dim not in ['latitude', 'longitude']:
58                dimensions[dim]=nextdim
59        crs=feature.getNativeCRS()
60        crss=[self._crscat.getCRS(crs).twoD]
61        if 'EPSG:4326' in crss:
62            crss.append('CRS:84')
63            crss.append('WGS84')       
64        #the units to return are the units of measure.
65        try:
66            units=feature.value.rangeSet.valueArray.valueComponent.uom
67        except:
68            units='unknown units'
69        return title, abstract, dimensions, units, crss
70       
71       
72     
73    def map(self, **kwargs):
74        """
75        Given csml.parser.Dataset object list the names of
76        all layers available.
77       
78        @return: A mapping of layer names to ILayer implementations.
79        @raise ValueError: If no layers are available for these keywords.
80        """
81        fileoruri=kwargs['fileoruri']
82        if fileoruri in self.layermapcache.keys():
83            #we've accessed this layer map before, get it from the cache dictionary
84            return self.layermapcache[fileoruri]
85         
86        ds = self.connector.getCsmlDoc(fileoruri)
87        layermap={}
88        self._crscat=csml.csmllibs.csmlcrs.CRSCatalogue()
89        for feature in csml.csmllibs.csmlextra.listify(ds.featureCollection.featureMembers):
90            title, abstract, dimensions, units, crss=self.getInfo(feature)
91            layermap[feature.id]=CSMLwmsLayer(title,abstract, dimensions, units, crss, feature)
92        if len(layermap) > 0:
93            self.layermapcache[fileoruri]=layermap
94            return layermap
95        else:
96            raise ValueError
97
98
99class CSMLwmsLayer(IwmsLayer):
100    """
101     representing a WMS layer.    Implements IwmsLayer
102
103    @ivar title: The layer title.  As seen in the Capabilities document.
104    @ivar abstract:  Abstract as seen in the Capabilities document.
105    @ivar dimensions: A dictionary of IDimension objects.
106    @ivar units: A string describing the units.
107    @ivar crss: A sequence of SRS/CRSs supported by this layer.
108
109    @todo: Do we need minValue/maxValue?
110
111    """
112   
113    def __init__(self, title, abstract, dimensions, units, crss, feature):
114        self.featureInfoFormats=None #NotImplemented
115        self.title=title
116        self.abstract=abstract
117        self.dimensions=dimensions
118        self.units=units
119        self.crss=crss
120        self._feature=feature
121        self.legendSize=(630,80)
122        self._minval=0
123        self._maxval=10.0 #dummy values
124        try: 
125            bb = self._feature.getCSMLBoundingBox().getBox()
126        except:
127            #default to global
128            bb=[-180,-90,180,90]
129        #convert 0 - 360 to -180, 180 as per common WMS convention
130        if abs(bb[2]-bb[0]) >= 359 and abs(bb[2]-bb[0]) < 361:
131            bb[0], bb[2]=-180, 180
132        self.wgs84BBox = bb
133        self.featureInfoFormats = ['text/html']
134        try:
135            self.wgs84BBox = self.getBBox('EPSG:4326')
136        except:
137            raise ValueError("Layer must provide a bounding box in EPSG:4326 "
138                             "coordinates for compatibility with WMS-1.3.0")
139        self.featureinfofilecache={} #used for caching netcdf file in getFeatureInfo
140       
141    def getBBox(self, crs):
142        """
143        @return: A 4-typle of the bounding box in the given coordinate
144            reference system.
145        """
146        #bb= self._feature.getCSMLBoundingBox().getBox()
147        #convert 0 - 360 to -180, 180 as per common WMS convention
148        #if abs(bb[2]-bb[0]) >= 359 and abs(bb[2]-bb[0]) < 361:
149        #    bb[0], bb[2]=-180, 180
150        #self.wgs84BBox = bb
151        return self.wgs84BBox
152        #raise NotImplementedError
153       
154    def getSlab(self, crs, dimValues=None, renderOpts={}):
155        """
156        Creates a slab of the layer in a particular CRS and set of
157        dimensions.
158
159        @param crs: The coordinate reference system.
160        @param dimValues: A mapping of dimension names to dimension values
161            as specified in the IDimension.extent
162        @param renderOpts: A generic mapping object for passing rendering
163            options
164        @return: An object implementing ILayerSlab
165        #create netcdf for whole lat/lon for given dimValues, use to init slab
166        """
167
168        log.debug('getSlab(%s, %s)' % (crs, dimValues))
169       
170        #unicode conversion
171        for dimval in dimValues:
172            if dimval != 'time':
173                dimValues[dimval]=float(dimValues[dimval])
174            else:
175                #remove any trailing Zs from time string
176                if dimValues[dimval] [-1:] in ['Z', 'z']:
177                    dimValues[dimval]=dimValues[dimval][:-1]
178        if type(self._feature) == csml.parser.GridSeriesFeature:
179            randomname= csml.csmllibs.csmlextra.getRandomID() + '.nc'
180            result= self._feature.subsetToGridSeries(config['tmpdir'], ncname=randomname, **dimValues)
181            #for now have to read netcdf back from
182            #disk (limitiation of CSML api)
183            netcdf=cdms.open(result[1])
184            #and then delete the temporary file
185            os.system('rm %s'%result[1])
186            bbox=self.getBBox(crs)
187            slab = CSMLwmsLayerSlab(netcdf, self, crs, dimValues, renderOpts, bbox)
188            self._minval=slab.minval #needed for legend rendering.
189            self._maxval=slab.maxval
190            return slab
191        else:
192            raise NotImplementedError
193       
194    def getCacheKey(self, crs, dimValues=None, renderOpts={}):
195        """
196        Create a unique key for use in caching a slab.
197
198        The intention here is that most of the work should be done when
199        instantiating an ILayerSlab object.  These can be cached by the
200        server for future use.  The server will first call getCacheKey()
201        for the slab creation arguments and if the key is in it's cache
202        it will use a pre-generated ILayerSlab object.
203
204        """
205        dimList = list(dimValues.items())
206        dimList.sort()
207        return '%s:%s:%s' % (self._feature.id, crs, dimList)
208
209    def getFeatureInfo(self, format, crs, point, dimValues):
210        """
211        Return a response string descibing the feature at a given
212        point in a given CRS.
213
214        Currently only "html" is supported as output format
215
216        @param format: One of self.featureInfoFormats.  Defines which
217            format the response will be in.
218        @param crs: One of self.crss
219        @param point: a tuple (x, y) in the supplied crs of the point
220            being selected.
221        @param dimValues: A mapping of dimension names to dimension values.
222        @return: A string containing the response.
223
224        """
225       
226        #cached netcdf is indexed by a tuple of feature id and dimvalues - i.e. typically a particular time and Z value for that feature.
227        #look in dictionary for cached copy, and if so use that as the file object.
228        dictindex=str((self._feature.id, dimValues))
229        if dictindex in self.featureinfofilecache:
230            log.debug('calling cache')
231            f=self.featureinfofilecache[dictindex]
232        else: #else, use the csml api to subset the feature afresh
233            log.debug('not calling cache')
234            randomname= csml.csmllibs.csmlextra.getRandomID() + '.nc'
235            result= self._feature.subsetToGridSeries(config['tmpdir'], ncname=randomname, **dimValues)
236            #for now have to read netcdf back from disk (limitation of CSML api)
237            f=cdms.open(result[1])
238            #append to cache:
239            self.featureinfofilecache[dictindex]=f
240            #and then delete the temporary file
241            os.system('rm %s'%result[1])
242       
243        netcdf = f(self.title)  #netcdf here is a cdms transient variable
244       
245       
246        #Now grab the netCDF object for the point specified.
247        #The reason for the 'cob' option is so that if the grid the data
248        #is defined on does not have a grid point at the point specified,
249        #we should  still get the nearest location
250       
251        t_point = netcdf(latitude=(point[1], point[1], 'cob'), longitude=(point[0], point[0], 'cob'))
252        #now get the value recorded at this location
253        value = t_point.getValue().tolist()
254        log.debug(value)
255        log.debug(t_point.fill_value())
256        #and the fill_value too
257        fill_value = t_point.fill_value()
258        #value is actually embedded in a multi dimensional list,
259        #so we need to extract the actual value from the list
260        while type(value) is list:
261                value = value[0]
262
263        #now check if the value is actually the fill_value rather than
264        #a value recorded at the point specified
265        log.debug('%s %s' % (value, fill_value))
266        if (2*fill_value) == value:
267                value = "No value found at position: "+str(point[1])+", "+str(point[0])
268        else:
269                value = "Value found at position: "+str(point[1])+", "+str(point[0])+" is: "+str(value)
270        # finally return the value
271        return value
272
273    def getLegendImage(self, orientation='horizontal', renderOpts={}):
274        """
275        Create an image of the colourbar for this layer.
276        @param orientation: Either 'vertical' or 'horizontal'
277        @return: A PIL image with labels
278
279        """
280        width = self.legendSize[0]
281        height = self.legendSize[1]
282        # if width and height specified in the GetLegend request parameters
283        # then use them instead of the default values
284        if 'width' in renderOpts:
285                width = renderOpts['width']
286        if 'height' in renderOpts:
287                height = renderOpts['height']
288        cmap = cm.get_cmap(config['colourmap'])
289        renderer=RGBARenderer(self._minval, self._maxval)
290       
291        if orientation =='vertical':
292            legendImage= renderer.renderColourbar(630, 30, cmap, isVertical=True)
293        else:
294            legendImage= renderer.renderColourbar(630, 30, cmap, isVertical=False)       
295        imageWithLabels=Image.new('RGBA', (630, 80), "white")
296        imageWithLabels.paste(legendImage, (0,0))
297        #add minvalue label
298        minvalueImg=self._simpletxt2image(str(self._minval), (49,25))
299        imageWithLabels.paste(minvalueImg,(0,40))
300        #add midvalue  label
301        midvalue=self._minval+(self._maxval-self._minval)/2
302        #add maxvalue label
303        midvalueImg=self._simpletxt2image(str(midvalue),(49,25))
304        imageWithLabels.paste(midvalueImg,(280,40))
305        #add maxvalue label
306        maxvalueImg=self._simpletxt2image(str(self._maxval), (49,25))
307        imageWithLabels.paste(maxvalueImg,(575,40))
308        #add units:
309        unitsImg=self._simpletxt2image('Units of measure: %s'%str(self.units), (200,25))
310        imageWithLabels.paste(unitsImg,(240,65))     
311#        return imageWithLabels                         
312        return imageWithLabels.resize((width, height)) 
313   
314    def _simpletxt2image(self, text, size):
315        image = Image.new('RGBA',size,"white")
316        fontfullpath = config['legendfont']
317        ifo = ImageFont.truetype(fontfullpath,16)
318        draw = ImageDraw.Draw(image)
319        draw.text((0, 0), text, font=ifo,fill=(100, 123, 165))
320        return image
321 
322   
323   
324   
325class CSMLwmsDimension(IwmsDimension):
326    """
327    implements IDimension
328    @ivar units: The units string.
329    @ivar extent: Sequence of extent values.
330
331    """
332   
333    def __init__(self, domain, dimname, unit):
334        self.units = unit
335        self.extent = []
336        #patch to handle current limitations of multiple time dimension scanning in csml.
337        if string.lower(self.units)[:10] in ['days_since', 'seconds_si', 'minutes_si', 'hours_sinc','months _sin', 'years_sinc']:
338            if type(domain[dimname][0]) is not str   :
339                tunits=self.units.replace('_', ' ')
340                for val in domain[dimname]:
341                    csmltime= csml.csmllibs.csmltime.UDtimeToCSMLtime(cdtime.reltime(float(val), tunits).tocomp())
342                    self.extent.append(csmltime)
343            else:
344                for val in domain[dimname]:
345                    self.extent.append(str(val))
346        else:
347            for val in domain[dimname]:
348                self.extent.append(str(val))
349        #for time axis replace units with iso string
350        if dimname == 'time':
351            self.units='ISO8601'
352       
353class CSMLwmsLayerSlab(IwmsLayerSlab):
354    """
355    Implements LayerSlab
356    Represents a particular horizontal slice of a WMS layer.
357
358    ILayerSlab objects are designed to be convenient to cache.
359    They should be pickleable to enable memcached support in the future.
360
361    @ivar layer: The source ILayer instance.
362    @ivar crs: The coordinate reference system.
363    @ivar dimValues: A mapping of dimension values of this view.
364    @ivar renderOpts: The renderOpts used to create this view.
365    @ivar bbox: The bounding box as a 4-tuple.
366    """
367
368    def __init__(self, netcdf, layer, crs, dimValues, renderOpts, bbox):
369        self._netcdf=netcdf
370        self.layer = layer
371        self.crs = crs
372        self.dimValues = dimValues
373        self.renderOpts=renderOpts
374        self.bbox=bbox
375       
376        #set colour map for ALL images from this slab
377        v=self._netcdf(layer.title)
378        tvar=v(squeeze=1)
379        #get the min and max values to use for the colourmapping.
380        #change the fill values to ensure they aren't picked up as false max/mins.
381        tvar.missing_value=999999999
382        value=tvar.getValue()
383        self.minval=min(min(l) for l in value)
384        tvar.missing_value=-0.000000001
385        value=tvar.getValue()
386        self.maxval=max(max(l) for l in value)
387
388       
389    def getImage(self, bbox, width, height):
390        """
391        Create an image of a sub-bbox of a given size.
392
393        @ivar bbox: A bbox 4-tuple.
394        @ivar width: width in pixels.` 
395        @ivar height: height in pixels.
396        @return: A PIL Image object.
397
398        """
399
400        log.debug('getImage(%s, %s, %s)' % (bbox, width, height))
401       
402        cmap = cm.get_cmap(config['colourmap'])
403
404        grid=Grid(self.layer, self._netcdf, bbox, width, height)
405        #how to handle varmin,varmax? ..read array?
406        #minval, maxval=genutil.minmax(grid.value)
407        #minval=min(min(l) for l in grid.value)
408        #maxval=max(max(l) for l in grid.value)
409        minval=self.minval
410        maxval=self.maxval
411        renderer=RGBARenderer(minval, maxval)         
412        return renderer.renderGrid(grid, bbox, width, height, cmap)
413   
414class Grid(object):
415    """A class encapsulating a simple regularly spaced, rectilinear
416    grid.  This is the only type of grid pywms is expected to
417    understand and adaptors should be provided to connect to
418    underlying implementations such as cdms or csml.
419
420    @cvar crs: Coordinate reference system
421
422    @ivar x0: coordinate of the lower left corner.
423    @ivar y0: coordinate of the lower left corner.
424    @ivar dx: The x grid spacing.
425    @ivar dy: The y grid spacing.
426    @ivar nx: The number of x grid points.
427    @ivar ny: The number of y grid points.
428    @ivar value: A masked array of the grid values.
429    @ivar ix: The dimension index of longidude in value
430    @ivar iy: The dimension index of latitude in value
431    @ivar long_name: The name of the field.
432    @ivar units: The units of the field.
433    """
434    def __init__(self, layer, netcdf, bbox, width, height):
435        #we know the axes are called latitude and longitude as the CSML code has written it:
436        v=netcdf(layer.title)       
437       
438        #sometimes these dimensions get squeezed out so need to get hold of the dx, dy spacing before the variable is subsetted.
439        lat=v.getLatitude()
440        lon=v.getLongitude()
441        self.dx=abs(lon[0]-lon[1])
442        self.dy=abs(lat[0]-lat[1])
443       
444        #now do the subset.
445        tvar=v(latitude=(bbox[1], bbox[3]), longitude=(bbox[0],bbox[2]),squeeze=1)
446        self.long_name=v.id
447        self.units=v.units
448                             
449        if type(tvar) == numpy.float32:
450            order ='xy'
451            self.value=numpy.ndarray(tvar)
452            self.ix=0
453            self.iy=1
454            self.x0=bbox[0]
455            self.y0=bbox[1]
456            self.nx=self.ny=1
457        else:    #it's a transient variable.
458            order=tvar.getOrder()
459            #array of data
460            self.value=tvar.getValue()
461            if order == 'xy':
462                self.ix=0
463                self.iy=1
464            else:
465                self.ix=1
466                self.iy=0
467            lat = tvar.getLatitude()
468            lon = tvar.getLongitude()     
469            if lon is not None:
470                self.x0=lon[0]
471                self.nx=len(lon)
472            else: #it's a single longitude value
473                self.x0=bbox[0]
474                self.nx= 1     
475            if lat is not None:
476                self.y0=lat[0]
477                self.ny=len(lat)
478            else: #it's a single latitude value
479                self.y0=bbox[1]
480                self.ny= 1
481
482       
Note: See TracBrowser for help on using the repository browser.