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

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

Added some additional text to the metadata title.

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
178        title, titleHeight = self._drawTitleToAxes(axes, renderer)
179       
180        txt, textHeight = self._drawMetadataTextToAxes(axes, renderer, self.metadataItems)
181
182        # fit the axis round the text
183
184        pos = axes.get_position()
185        newpos = Bbox( [[pos.x0,  pos.y1 - (titleHeight + textHeight) / height], [pos.x1, pos.y1]] )
186        axes.set_position(newpos )
187
188        # position the text below the title
189
190        newAxisHeight = (newpos.y1 - newpos.y0) * height
191        txt.set_position( (0.02, 0.98 - (titleHeight/newAxisHeight) ))
192
193        for loc, spine in axes.spines.iteritems():
194            spine.set_edgecolor(borderColor)
195       
196        # Draw heading box
197       
198        headingBoxHeight = titleHeight - 1
199       
200        axes.add_patch(Rectangle((0, 1.0 - (headingBoxHeight/newAxisHeight)), 1, (headingBoxHeight/newAxisHeight),
201                       facecolor=borderColor,
202                      fill = True,
203                      linewidth=0))
204
205        # reduce the figure height
206       
207        originalHeight = fig.get_figheight()
208        pos = axes.get_position()
209        topBound = 20 / float(dpi)
210       
211        textHeight = (pos.y1 - pos.y0) * originalHeight
212       
213        newHeight = topBound * 2 + textHeight
214       
215        # work out the new proportions for the figure
216       
217        border = topBound / float(newHeight)
218        newpos = Bbox( [[pos.x0,  border], [pos.x1, 1 - border]] )
219        axes.set_position(newpos )
220       
221        fig.set_figheight(newHeight)
222       
223        return figureToImage(fig)
224   
225    def _drawMetadataTextToAxes(self, axes, renderer, metadataItems):
226        '''
227        Draws the metadata text to the axes
228       
229        @param axes: the axes to draw the text on
230        @type axes: matplotlib.axes.Axes
231        @param renderer: the matplotlib renderer to evaluate the text size
232        @param metadataItems: a list of metadata items to get the text form
233       
234        @return: the text object, the total metadata text height in pixels
235        '''
236       
237        lines = self.metadataItems
238        text = '\n'.join(lines)
239        txt = axes.text(0.02, 0.98 ,text,
240                fontdict=metadataFont,
241                horizontalalignment='left',
242                verticalalignment='top',)
243
244        extent = txt.get_window_extent(renderer)
245       
246        textHeight = (extent.y1 - extent.y0 + 10)
247        return txt, textHeight       
248
249    def _drawTitleToAxes(self, axes, renderer):
250        '''
251        Draws the metadata tile text onto the axes
252       
253        @return: the text object, the height of the title text in pixels
254        '''
255       
256        titleText = self._getTitleText()
257
258        title = axes.text(0.02,0.98,titleText,
259                fontdict=titleFont,
260                horizontalalignment='left',
261                verticalalignment='top',)
262
263        extent = title.get_window_extent(renderer)
264        titleHeight = (extent.y1 - extent.y0 + 8)
265        return title, titleHeight       
266
267    def _getTitleText(self):
268       
269        titleText = "Plot Metadata:"
270        additionalText = config.get('additional_figure_text', '')
271       
272        if additionalText != "":
273       
274            if additionalText.find('<date>') > 0:
275                timeString = time.strftime("%Y-%m-%D %H:%M:%S", time.localtime())
276                additionalText = additionalText.replace("<date>", timeString)
277           
278            titleText += " %s" % (additionalText,)
279       
280        return titleText
281
282    def _buildMetadataItems(self, layerInfoList):
283        items = []
284       
285        for i in range(len(layerInfoList)):
286            li = layerInfoList[i]
287            j = i + 1
288            items.append("%s:endpoint = %s layerName = %s" % (j, li.endpoint, li.layerName))
289            items.append("%s:params = %s" % (j, li.params))
290               
291        return items
292
293class LayerInfoBuilder(object):
294   
295    def __init__(self, requestParams):
296       
297        self.params = {}
298       
299        for k, v in requestParams.items():
300            self.params[k.upper()] = v
301       
302    def buildInfo(self):
303       
304        infoList = []
305       
306        for i in range(1,11):
307            s = '%s_' % (i)
308           
309            #find all the parameters that correspond to layer num i
310            keys = filter(lambda x: x.find(s) == 0, self.params.keys())
311           
312            #if there are no parameters for this layer there won't be any more
313            if len(keys) == 0:
314                break
315           
316            if i == 10:
317                raise Exception("Can's support 10 layers.")
318           
319           
320            #get the parameters for this layer (taking the #_ off the front)
321            p = {}
322            for k in keys:
323                if not self.params[k] in ['', None]: 
324                    p[k[len(s):]] = self.params[k]
325           
326            p.pop('REQUEST')
327           
328            log.debug("p = %s" % (p,))
329            name = p['LAYERS']
330            endpoint = p['ENDPOINT']
331           
332            try:
333                legendURL = self._getLegendURL(endpoint, name, p)
334            except:
335                log.exception("Failed to get legend URL from endpoint=%s, layer=%s" % (endpoint, name))
336                legendURL = None
337           
338           
339            li = LayerInfo(endpoint, name, p, legendURL)
340            infoList.append(li)
341           
342        return infoList
343
344    def _getWMCLayer(self, root, ns, layerName):
345
346        for l in self._findElementsByName(root, 'Layer', ns):
347            nameElt = self._getFirstChildElt(l, 'Name', ns)
348           
349            if nameElt is None: continue
350           
351            if nameElt.text == layerName:
352                return l
353       
354        return None
355
356    def _getStyleELt(self, layerElt, ns="", style=None):
357       
358        styleElts = self._getChildElementsByName(layerElt, 'Style', ns)
359           
360        if len(styleElts)  == 0: return None
361       
362        if style is None: return styleElts[0]
363       
364        for sElt in styleElts:
365            nameElt = self._getFirstChildElt(sElt, 'name', ns)
366           
367            if nameElt is None: continue
368           
369            if nameElt.text == style:
370                return sElt
371       
372        return None           
373   
374    def _getLegendURL(self, endpoint, name, p):
375           
376        reqParams = {'REQUEST':'GetCapabilities', 'SERVICE':'WMS'}
377       
378        requestURL = endpoint +"?" + urllib.urlencode(reqParams)
379       
380        req = urllib2.Request(requestURL)
381        req.add_header('Cookie', request.headers.get('Cookie', ''))
382       
383        filehandle = openURL(req)
384        s = filehandle.read()
385        filehandle.close()
386       
387        root = ET.XML(s)
388       
389        ns = root.tag[root.tag.find("{")+1:root.tag.find("}")]
390       
391        layerElt = self._getWMCLayer(root, ns, name)
392        if layerElt is None:
393            return None     
394       
395        styleElt = self._getStyleELt(layerElt, ns, p.get('STYLE', None))
396        if styleElt is None:
397            return None
398       
399        legURL = self._getLegURLFromStyle(styleElt, ns)
400       
401       
402        legURL = parseEndpointString(legURL, p)
403       
404        return legURL
405       
406       
407   
408    def _getLegURLFromStyle(self, styleElt, ns):
409        legElt = self._getFirstChildElt(styleElt,  'LegendURL' , ns)
410       
411        if legElt is None: return None
412       
413        orElt = self._getFirstChildElt(legElt, 'OnlineResource', ns)
414       
415        if orElt is None: return None
416       
417        return orElt.attrib['{http://www.w3.org/1999/xlink}href']
418       
419       
420       
421    def _findElementsByName(self, root, name, ns=""):
422        if ns != "":
423            name = "{%s}%s" % (ns, name)
424           
425        return filter(lambda x: x.tag == name, root.getiterator())
426   
427    def _getFirstChildElt(self, root, name, ns=""):
428        elts = self._getChildElementsByName(root, name, ns)
429        if len(elts) == 0:
430            return None
431       
432        return elts[0]
433   
434    def _getChildElementsByName(self, root, name, ns=""):
435        if ns != "":
436            name = "{%s}%s" % (ns, name)
437        return filter(lambda x: x.tag == name, root.getchildren())       
438
439
440class LegendImageBuilder(object):
441   
442    def __init__(self):
443       
444        self.border = 20
445        imageFolder = config['image_folder']
446       
447        self.legendLabels = {
448            1  : imageFolder + "/one.png",
449            2  : imageFolder + "/two.png",
450            3  : imageFolder + "/three.png",
451            4  : imageFolder + "/four.png",
452            5  : imageFolder + "/five.png",
453            6  : imageFolder + "/six.png",
454            7  : imageFolder + "/seven.png",
455            8  : imageFolder + "/eight.png",
456            9  : imageFolder + "/nine.png",
457            10 : imageFolder + "/ten.png",
458        }
459
460   
461    def buildLegendImage(self, infoList, width):
462       
463        legendImages = self._getLegendImages(infoList)
464       
465        height = self._calculateHeight(legendImages)
466       
467        combinedImage = Image.new('RGBA', (width, height))
468       
469        self._pasteImages(combinedImage, legendImages)
470       
471        return combinedImage
472   
473   
474    def _pasteImages(self, combinedImage, legendImages):
475       
476        top = self.border
477       
478        for labelImage, legendImage in legendImages:
479           
480            labelDownShift = 0 
481            legendDownShift = 0
482             
483            if labelImage.size[1] > legendImage.size[1]:
484                legendDownShift = self._getDownShift(legendImage.size[1], labelImage.size[1])
485            else:
486                labelDownShift = self._getDownShift(labelImage.size[1], legendImage.size[1])
487           
488            totalWidth = labelImage.size[0] + legendImage.size[0] + (self.border * 3)
489            rightShift = self._getRightShift(totalWidth, combinedImage.size[0]) 
490           
491            log.debug("legendDownShift = %s, labelDownShift = %s, rightShift = %s" % (legendDownShift, labelDownShift, rightShift,))
492           
493            combinedImage.paste(labelImage, (self.border + rightShift, top + labelDownShift))           
494            combinedImage.paste(legendImage, (rightShift + (self.border * 2) + labelImage.size[1], top + legendDownShift))
495
496            legHeight = max([labelImage.size[1], legendImage.size[1]])
497            top += (legHeight + self.border ) 
498           
499           
500    def _getDownShift(self, shortHeight, tallHeight):
501       
502        return int( (tallHeight - shortHeight ) / 2.0 )
503   
504    def _getRightShift(self, smallWidth, wideWidth):
505        return int( (wideWidth - smallWidth) / 2.0 )
506           
507   
508    def _calculateHeight(self, legendImages):
509       
510        height = self.border
511        for labelImage, legendImage in legendImages:
512            height += max([labelImage.size[1], legendImage.size[1]])
513            height += self.border
514           
515        return height
516                           
517       
518    def _getLegendImages(self, infoList):
519       
520        legendImages = []
521        for i in range(len(infoList)):
522            j = i + 1
523            info = infoList[i]
524           
525            if info.legendURL is None:
526                continue
527           
528            req = urllib2.Request(info.legendURL)
529            req.add_header('Cookie', request.headers.get('Cookie', ''))
530       
531            filehandle = openURL(req)
532            buffer = StringIO(filehandle.read())
533            im = Image.open(buffer)
534            filehandle.close()
535           
536            log.debug("info.legendURL = %s, im.size = %s" % (info.legendURL, im.size,))
537           
538            labelImage = Image.open(self.legendLabels[j])
539           
540            legendImages.append( (labelImage, im) )
541           
542        return legendImages
543       
544
545def figureToImage(fig):
546   
547    canvas = FigureCanvas(fig)
548   
549    buffer = StringIO()
550    canvas.print_figure(buffer, dpi=fig.get_dpi(), facecolor=fig.get_facecolor(), edgecolor=fig.get_edgecolor())
551    buffer.seek(0)   
552    im = Image.open(buffer)
553   
554    return im
Note: See TracBrowser for help on using the repository browser.