source: cowsclient/trunk/cowsclient/lib/figure_builder.py @ 6517

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/cowsclient/trunk/cowsclient/lib/figure_builder.py@6517
Revision 6517, 16.6 KB checked in by pnorton, 11 years ago (diff)

First attempt at extending the make figure code to include metadata and legends.

Line 
1
2from cowsclient.lib.wmc_util import openURL
3import urllib, urllib2, time
4import copy, logging
5
6import libxml2dom
7
8try:
9    from PIL import Image, ImageChops
10except:
11    import Image, ImageChops
12   
13from cowsclient.lib.base import request
14from cowsclient.lib.png_combine import merge
15from cowsclient.lib.wmc_util import parseEndpointString
16
17from cStringIO import StringIO
18
19log = logging.getLogger(__name__)
20
21from matplotlib.backends.backend_cairo import FigureCanvasCairo as FigureCanvas
22from matplotlib.figure import Figure
23from matplotlib.backends.backend_cairo import RendererCairo as Renderer
24from matplotlib.transforms import Bbox
25from matplotlib.patches import Rectangle
26
27from pylons import config
28
29import xml.etree.ElementTree as ET
30
31
32#TODO: this code has been written rather quickly and needs come cleaning up
33
34class LayerInfo(object):
35   
36    def __init__(self, endpoint, layerName, params, legendURL):
37        self.endpoint = endpoint
38        self.layerName = layerName
39        self.params = params
40        self.legendURL = legendURL
41
42
43class FigureBuilder(object):
44   
45   
46    def __init__(self, originalParams):
47       
48        self.infoBuilder = LayerInfoBuilder(originalParams.copy())
49        self.metadataBuilder = MetadataImageBuilder()
50        self.legendBuilder = LegendImageBuilder()
51       
52    def buildImage(self, mapImage, points):
53        '''
54        Builds a composite image of the metadata, map and legends
55       
56        @param mapImage: the image of the map
57        @type mapImage: PIL Image
58        @param points: the points on the map that locate the four corners
59        (this is excluding the labels)
60        @type points: 4-tuple
61        '''
62       
63        infoList = self.infoBuilder.buildInfo()
64       
65        log.debug("mapImage.size = %s" % (mapImage.size,))
66       
67        metadataImage = self.metadataBuilder.buildMetadataImage(infoList,
68                                            mapImage.size[0])
69
70        legendImage = self.legendBuilder.buildLegendImage(infoList,
71                                                          mapImage.size[0])
72       
73        log.debug("legendImage.size = %s" % (legendImage.size,))
74        log.debug("metadataImage.size = %s" % (metadataImage.size,))
75       
76       
77        # if the top of the map is a long way from the top of the map image then
78        # we can overlay the metadata and legend images slightly.
79       
80        log.debug("points = %s" % (points,))
81       
82        (left, bottom, right, top) = points
83        topOverlap = mapImage.size[1] - top - 30
84        if topOverlap < 0:
85            topOverlap = 0
86       
87        bottomOverlap = bottom - 50
88        if bottomOverlap < 0:
89            bottomOverlap = 0
90       
91        log.debug("topOverlap = %s, bottomOverlap = %s" % (topOverlap, bottomOverlap,))
92        compositeIm = Image.new('RGBA', (mapImage.size[0], 
93                                         metadataImage.size[1] + mapImage.size[1] + legendImage.size[1]\
94                                         - topOverlap - bottomOverlap))
95       
96       
97       
98        legendBox = (0, metadataImage.size[1] + mapImage.size[1]- topOverlap - bottomOverlap,
99               legendImage.size[0], mapImage.size[1] + metadataImage.size[1] + legendImage.size[1] - topOverlap - bottomOverlap)
100       
101       
102        mapBox = (0, metadataImage.size[1] - topOverlap,
103               mapImage.size[0], mapImage.size[1] + metadataImage.size[1]  - topOverlap)
104       
105        metadataBox = (0, 0, metadataImage.size[0], metadataImage.size[1])
106
107        compositeIm.paste(mapImage, mapBox)
108        compositeIm.paste(metadataImage, metadataBox)
109        compositeIm.paste(legendImage, legendBox)       
110       
111       
112       
113        return compositeIm
114
115   
116    def _getLayerParams(self, requestParams, baseParams):
117        """
118        Splits up the request parameters into parameters for the each individual
119        layer by using the '1_', '2_' at the start.
120        """
121       
122        layerParams = []
123       
124        for i in range(1, 11):
125            s = '%s_' % (i)
126            #find all the parameters that correspond to layer num i
127            keys = filter(lambda x: x.find(s) == 0, requestParams.keys())
128                       
129            if len(keys) == 0:
130                break
131           
132            if i == 10:
133                raise Exception("Can's support 10 layers.")
134           
135            p = {}
136            for k in keys:
137                if not requestParams[k] in ['', None]: 
138                    p[k[len(s):]] = requestParams[k]
139           
140            # apply the defaults
141            for k,v in baseParams.items():
142                p.setdefault(k,v)
143               
144            layerParams.append(p)
145           
146        return layerParams
147
148       
149       
150
151metadataFont = {'weight':'normal',
152                'family':'sans-serif',
153                'size':'12'}
154
155titleFont = {'weight':'normal',
156             'family':'sans-serif',
157             'size':'16'}
158       
159borderColor = 'grey'
160       
161class MetadataImageBuilder(object):
162   
163    def __init__(self):
164        pass
165   
166    def buildMetadataImage(self, layerInfoList, width):
167
168        self.metadataItems = self._buildMetadataItems(layerInfoList)
169        self.width = width
170       
171        width=self.width;height=1600;dpi=100;transparent=False
172        figsize=(width / float(dpi), height / float(dpi))
173        fig = Figure(figsize=figsize, dpi=dpi, facecolor='w', frameon=(not transparent))
174        axes = fig.add_axes([0.04, 0.04, 0.92, 0.92],  frameon=True,xticks=[], yticks=[])
175        renderer = Renderer(fig.dpi)
176
177        lines = self.metadataItems
178       
179        text = '\n'.join(lines)
180
181        title = axes.text(0.02,0.98,"Plot Metadata:",
182                fontdict=titleFont,
183                horizontalalignment='left',
184                verticalalignment='top',)
185
186        extent = title.get_window_extent(renderer)
187        titleHeight = (extent.y1 - extent.y0 + 8)
188       
189        txt = axes.text(0.02, 0.98 ,text,
190                fontdict=metadataFont,
191                horizontalalignment='left',
192                verticalalignment='top',)
193
194        extent = txt.get_window_extent(renderer)
195       
196        textHeight = (extent.y1 - extent.y0 + 10)
197       
198        log.debug("textHeight = %s, titleHeight = %s" % (textHeight, titleHeight,))
199       
200        # fit the axis round the text
201
202        pos = axes.get_position()
203        newpos = Bbox( [[pos.x0,  pos.y1 - (titleHeight + textHeight) / height], [pos.x1, pos.y1]] )
204       
205        axes.set_position(newpos )
206
207        # position the text below the title
208
209        newAxisHeight = (newpos.y1 - newpos.y0) * height
210        txt.set_position( (0.02, 0.98 - (titleHeight/newAxisHeight) ))
211
212        for loc, spine in axes.spines.iteritems():
213            spine.set_edgecolor(borderColor)
214       
215        # Draw heading box
216       
217        headingBoxHeight = titleHeight - 3
218       
219        axes.add_patch(Rectangle((0, 1.0 - (headingBoxHeight/newAxisHeight)), 1, (headingBoxHeight/newAxisHeight),
220                       facecolor=borderColor,
221                      fill = True,
222                      linewidth=0))
223
224        # reduce the figure height
225       
226        originalHeight = fig.get_figheight()
227        pos = axes.get_position()
228        topBound = 20 / float(dpi)
229       
230        textHeight = (pos.y1 - pos.y0) * originalHeight
231       
232        newHeight = topBound * 2 + textHeight
233        log.debug("newHeight = %s, originalHeight = %s, topBound = %s" % (newHeight, originalHeight, topBound))
234       
235        # work out the new proportions for the figure
236       
237        border = topBound / float(newHeight)
238        newpos = Bbox( [[pos.x0,  border], [pos.x1, 1 - border]] )
239        axes.set_position(newpos )
240       
241        fig.set_figheight(newHeight)
242       
243        return figureToImage(fig)
244
245    def _buildMetadataItems(self, layerInfoList):
246        items = []
247       
248        for i in range(len(layerInfoList)):
249            li = layerInfoList[i]
250            j = i + 1
251            items.append("%s:endpoint = %s layerName = %s" % (j, li.endpoint, li.layerName))
252            items.append("%s:params = %s" % (j, li.params))
253               
254        return items
255
256class LayerInfoBuilder(object):
257   
258    def __init__(self, requestParams):
259       
260        self.params = {}
261       
262        for k, v in requestParams.items():
263            self.params[k.upper()] = v
264       
265    def buildInfo(self):
266       
267        infoList = []
268       
269        for i in range(1,11):
270            s = '%s_' % (i)
271           
272            #find all the parameters that correspond to layer num i
273            keys = filter(lambda x: x.find(s) == 0, self.params.keys())
274           
275            #if there are no parameters for this layer there won't be any more
276            if len(keys) == 0:
277                break
278           
279            if i == 10:
280                raise Exception("Can's support 10 layers.")
281           
282           
283            #get the parameters for this layer (taking the #_ off the front)
284            p = {}
285            for k in keys:
286                if not self.params[k] in ['', None]: 
287                    p[k[len(s):]] = self.params[k]
288           
289            p.pop('REQUEST')
290           
291            log.debug("p = %s" % (p,))
292            name = p['LAYERS']
293            endpoint = p['ENDPOINT']
294           
295            #info = LayerInfo(endpoint=endpoint, layerName=name)
296            log.debug("name = %s, endpoint = %s" % (name, endpoint,))
297
298            try:
299                legendURL = self._getLegendURL(endpoint, name, p)
300            except:
301                log.exception("Failed to get legend URL from endpoint=%s, layer=%s" % (endpoint, name))
302                legendURL = None
303           
304            log.debug("legendURL = %s" % (legendURL,))
305           
306            li = LayerInfo(endpoint, name, p, legendURL)
307            log.debug("li.legendURL = %s" % (repr(li.legendURL),))
308            infoList.append(li)
309           
310        return infoList
311
312    def _getWMCLayer(self, root, ns, layerName):
313
314        for l in self._findElementsByName(root, 'Layer', ns):
315            nameElt = self._getFirstChildElt(l, 'Name', ns)
316           
317            if nameElt is None: continue
318           
319            if nameElt.text == layerName:
320                return l
321       
322        return None
323
324    def _getStyleELt(self, layerElt, ns="", style=None):
325       
326        styleElts = self._getChildElementsByName(layerElt, 'Style', ns)
327           
328        if len(styleElts)  == 0: return None
329       
330        if style is None: return styleElts[0]
331       
332        for sElt in styleElts:
333            nameElt = self._getFirstChildElt(sElt, 'name', ns)
334           
335            if nameElt is None: continue
336           
337            if nameElt.text == style:
338                return sElt
339       
340        return None           
341   
342    def _getLegendURL(self, endpoint, name, p):
343           
344        reqParams = {'REQUEST':'GetCapabilities', 'SERVICE':'WMS'}
345       
346        requestURL = endpoint +"?" + urllib.urlencode(reqParams)
347       
348        req = urllib2.Request(requestURL)
349        req.add_header('Cookie', request.headers.get('Cookie', ''))
350       
351        filehandle = openURL(req)
352        s = filehandle.read()
353        filehandle.close()
354       
355        root = ET.XML(s)
356       
357        ns = root.tag[root.tag.find("{")+1:root.tag.find("}")]
358       
359        layerElt = self._getWMCLayer(root, ns, name)
360        if layerElt is None:
361            return None     
362       
363        styleElt = self._getStyleELt(layerElt, ns, p.get('STYLE', None))
364        if styleElt is None:
365            return None
366       
367        legURL = self._getLegURLFromStyle(styleElt, ns)
368       
369       
370        legURL = parseEndpointString(legURL, p)
371       
372        log.debug("legURL = %s" % (legURL,))
373        return legURL
374       
375       
376   
377    def _getLegURLFromStyle(self, styleElt, ns):
378        legElt = self._getFirstChildElt(styleElt,  'LegendURL' , ns)
379       
380        if legElt is None: return None
381       
382        orElt = self._getFirstChildElt(legElt, 'OnlineResource', ns)
383       
384        if orElt is None: return None
385       
386        return orElt.attrib['{http://www.w3.org/1999/xlink}href']
387       
388       
389       
390    def _findElementsByName(self, root, name, ns=""):
391        if ns != "":
392            name = "{%s}%s" % (ns, name)
393           
394        return filter(lambda x: x.tag == name, root.getiterator())
395   
396    def _getFirstChildElt(self, root, name, ns=""):
397        elts = self._getChildElementsByName(root, name, ns)
398        if len(elts) == 0:
399            return None
400       
401        return elts[0]
402   
403    def _getChildElementsByName(self, root, name, ns=""):
404        if ns != "":
405            name = "{%s}%s" % (ns, name)
406        return filter(lambda x: x.tag == name, root.getchildren())       
407
408
409class LegendImageBuilder(object):
410   
411    def __init__(self):
412       
413        self.border = 20
414        imageFolder = config['image_folder']
415       
416        self.legendLabels = {
417            1  : imageFolder + "/one.png",
418            2  : imageFolder + "/two.png",
419            3  : imageFolder + "/three.png",
420            4  : imageFolder + "/four.png",
421            5  : imageFolder + "/five.png",
422            6  : imageFolder + "/six.png",
423            7  : imageFolder + "/seven.png",
424            8  : imageFolder + "/eight.png",
425            9  : imageFolder + "/nine.png",
426            10 : imageFolder + "/ten.png",
427        }
428
429   
430    def buildLegendImage(self, infoList, width):
431        log.debug("self.legendLabels = %s" % (self.legendLabels,))
432       
433        legendImages = self._getLegendImages(infoList)
434       
435        height = self._calculateHeight(legendImages)
436       
437        log.debug("width = %s, height = %s" % (width, height,))
438       
439        combinedImage = Image.new('RGBA', (width, height))
440       
441        self._pasteImages(combinedImage, legendImages)
442       
443        return combinedImage
444   
445   
446    def _pasteImages(self, combinedImage, legendImages):
447       
448        top = self.border
449       
450        for labelImage, legendImage in legendImages:
451           
452            labelDownShift = 0 
453            legendDownShift = 0
454             
455            if labelImage.size[1] > legendImage.size[1]:
456                legendDownShift = self._getDownShift(legendImage.size[1], labelImage.size[1])
457            else:
458                labelDownShift = self._getDownShift(labelImage.size[1], legendImage.size[1])
459           
460            totalWidth = labelImage.size[0] + legendImage.size[0] + (self.border * 3)
461            rightShift = self._getRightShift(totalWidth, combinedImage.size[0]) 
462           
463            log.debug("legendDownShift = %s, labelDownShift = %s, rightShift = %s" % (legendDownShift, labelDownShift, rightShift,))
464           
465            combinedImage.paste(labelImage, (self.border + rightShift, top + labelDownShift))           
466            combinedImage.paste(legendImage, (rightShift + (self.border * 2) + labelImage.size[1], top + legendDownShift))
467
468            legHeight = max([labelImage.size[1], legendImage.size[1]])
469            top += (legHeight + self.border ) 
470           
471           
472    def _getDownShift(self, shortHeight, tallHeight):
473       
474        return int( (tallHeight - shortHeight ) / 2.0 )
475   
476    def _getRightShift(self, smallWidth, wideWidth):
477        return int( (wideWidth - smallWidth) / 2.0 )
478           
479   
480    def _calculateHeight(self, legendImages):
481       
482        height = self.border
483        for labelImage, legendImage in legendImages:
484            height += max([labelImage.size[1], legendImage.size[1]])
485            height += self.border
486           
487        return height
488                           
489       
490    def _getLegendImages(self, infoList):
491       
492        legendImages = []
493        for i in range(len(infoList)):
494            j = i + 1
495            info = infoList[i]
496           
497            if info.legendURL is None:
498                continue
499           
500            req = urllib2.Request(info.legendURL)
501            req.add_header('Cookie', request.headers.get('Cookie', ''))
502       
503            filehandle = openURL(req)
504            buffer = StringIO(filehandle.read())
505            im = Image.open(buffer)
506            filehandle.close()
507           
508            log.debug("info.legendURL = %s, im.size = %s" % (info.legendURL, im.size,))
509           
510            labelImage = Image.open(self.legendLabels[j])
511           
512            legendImages.append( (labelImage, im) )
513           
514        return legendImages
515       
516
517def figureToImage(fig):
518   
519    canvas = FigureCanvas(fig)
520   
521    buffer = StringIO()
522    canvas.print_figure(buffer, dpi=fig.get_dpi(), facecolor=fig.get_facecolor(), edgecolor=fig.get_edgecolor())
523    buffer.seek(0)   
524    im = Image.open(buffer)
525   
526    return im
Note: See TracBrowser for help on using the repository browser.