source: DPPP/kml/csml2kml/python/csml2kml/csml2kml/WMSCapabilities.py @ 3537

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/DPPP/kml/csml2kml/python/csml2kml/csml2kml/WMSCapabilities.py@3537
Revision 3537, 16.4 KB checked in by mkochan, 12 years ago (diff)

Changed WMSLayer convertor to save to KMZ files.

Line 
1import os
2import re
3from pylab import dates     # a very good date/time module from matplotviz -- allows years < 1970
4from KML import *
5
6# [TODO]
7#        * Resolve maxDirDepth
8
9wmsXmlNamespace = 'http://www.opengis.net/wms'
10
11def wmsLayerFactory(layerElement):
12    '''
13    [DOC]
14    '''
15    name = layerElement.find('{%s}Name' % wmsXmlNamespace).text
16    title = layerElement.find('{%s}Title' % wmsXmlNamespace).text
17    abstract = layerElement.find('{%s}Abstract' % wmsXmlNamespace).text
18    childElements = layerElement.findall('{%s}Layer' % wmsXmlNamespace)
19    childWmsLayers = []
20    for childElement in childElements:
21        childWmsLayer = wmsLayerFactory(childElement)
22        childWmsLayers.append(childWmsLayer)
23    if childElements != []:
24        return WMSLayer(name, title, abstract, childWmsLayers)       
25    else:
26        dimensionElements = layerElement.findall('{%s}Dimension' % wmsXmlNamespace)
27        for dimensionElement in dimensionElements:
28            if dimensionElement.get('name') == 'time':
29                timesteps = map( dates.dateutil.parser.parse, dimensionElement.text.split(',') )
30        return BottomWMSLayer(name, title, abstract, timesteps)
31
32class WMSLayer:
33    '''
34    [DOC]
35    '''
36
37    def __init__(self, name, title, abstract, children):
38        self.name = name
39        self.title = title
40        self.abstract = abstract
41        self.children = children
42
43    def __repr__(self):
44        return str(vars(self))
45
46    def parseXML(self, layerElement):
47        raise NotImplementedError('Use the wmsLayerFactory() function instead.')
48
49    def toKML(self, wmsRequestConfigElement, viewTypes):
50        # ignore wmsRequestConfigElement, viewTypess
51        kmlLayerFolder = KMLFolder(self.title, [], opened = False, visible = False)
52        for childWmsLayer in self.children:
53            kmlLayerFolder.children.append( childWmsLayer.toKML(wmsRequestConfigElement, viewTypes) )
54        return kmlLayerFolder
55
56class BottomWMSLayer(WMSLayer):
57
58    '''[DOC]'''
59   
60    def __init__(self, name, title, abstract, timesteps):
61
62        self.name = name
63        self.title = title
64        self.abstract = abstract
65        # but no self.children
66        self.timesteps = timesteps
67
68    def _parseName(self):
69        mo = re.match('(.+)\:(.+)\:(.+)', self.name)
70        if mo:
71            (modelName, scenarioName, rest) = mo.groups()
72        else:
73            (modelName, scenarioName, rest) = (None, None, self.name)
74
75        mo2 = re.match('(clim|change)\_(\d+)\/(.+)', rest)
76        if mo2:
77            (type, periodText, description) = mo2.groups()
78            period = int(periodText)
79        else:
80            raise ValueError('Cannot parse in layer name')
81
82        return (type, period, description, modelName, scenarioName)
83
84    def getType(self):
85        return self._parseName()[0]
86
87    def getPeriod(self):
88        '''@return The period length (integer)'''
89        return self._parseName()[1]
90   
91    def getDescription(self):
92        return self._parseName()[2]
93
94    def getModelName(self):
95        return self._parseName()[3]
96
97    def getScenarioName(self):
98        return self._parseName()[4]
99
100    def toKML(self, wmsRequestConfigElement, viewTypes):
101        '''
102        @param viewTyps: A list of View classes (but not instances), which define what kinds of view we are going
103                         to use to look at the data.
104        @return:         A KML.KMLFolder object representing a <kml:Folder> element with lots of <kml:GroundOverlay>
105                         elements, each standing for a different time segment.
106        '''
107        kmlLayerFolder = KMLFolder(self.title, [], visible = False, opened = False)
108        for viewType in viewTypes:
109            view = viewType(self, wmsRequestConfigElement)
110            kmlLayerFolder.children.append( view.toKML() )
111        return kmlLayerFolder
112
113class View:
114    '''
115    Determines how BottomWMSLayer data can be viewed, i.e. how it can be converted into KML so it can be viewed
116    in Google Earth. In particular, it defines logical transforms of time-points into time-spans.
117    '''
118
119    def __init__(self, layer, wmsRequestConfigElement):
120        '''
121        Initialize the view.
122        @param layer: Some views (not all) may need to "see" the layer data (although some ignore it).
123        '''
124        self.layer = layer
125        self.wmsRequestConfigElement = wmsRequestConfigElement
126        self.description = None
127
128    def areCategoriesListedExplicitly(self):
129        '''
130        @returns: A boolean value that signifies whether the self.toKML() method should list the categories
131                  explicitly (in separate KMLFolder's). Must be implemented by all derived classes.
132        '''
133        raise NotImplementedError()
134
135    def getLogicalTimespan(self, timestep):
136        '''
137        Abstract method, defined in derived classes.
138        Translates a single time step into a time span.
139        @param timestep: The date step (a datetime object)
140        @return: The (timespanStart, timespanEnd) tuple (both are datetime objects)
141        '''
142        pass
143
144    def getCategory(self, timestep):
145        pass
146
147    def getCategoryDescription(self, category):
148        '''Abstract method, defined in derived classes. Get a human-readable description of the category.'''
149        pass
150
151    def _getSameDate(self, timestep):
152        return timestep
153
154    def _getFirstDayOfMonth(self, timestep):
155        return timestep.replace(day=1)
156
157    def _getMonthHence(self, timestep):
158        if timestep.month+1 <= 12:
159            return timestep.replace(month=timestep.month+1)
160        else:
161            return timestep.replace(year=timestep.year+1, month=1)
162   
163    def _getYearHence(self, timestep):
164        return timestep.replace(year=timestep.year+1)
165
166    def _getHalfPeriodEarlier(self, timestep):
167        return timestep.replace(year = timestep.year-self.layer.getPeriod()/2)
168
169    def _getHalfPeriodLater(self, timestep):
170        return timestep.replace(year = timestep.year+self.layer.getPeriod()/2)
171
172    def toKML(self):
173
174        def buildWMSRequest(timestep):
175            ''' Build a WMS request '''
176
177            # We will be using configuration for WMS request
178            c = self.wmsRequestConfigElement
179
180            # Set request configuration parameters
181            url = c.find('URL').text
182            serviceVersion = c.find('ServiceVersion').text
183            imageFormat = c.find('ImageFormat').text
184            imageWidth = c.find('ImageWidth').text
185            imageHeight = c.find('ImageHeight').text
186            crs = c.find('CRS').text
187
188            bBox = '-180,-90,180,90'
189
190            # If the timezone is UTC (which in ISO form would look like 'yyyy-mm-ddThh:mm:ss+00:00'),
191            # then replace it with 'Z'.
192            timestepString = timestep.isoformat()
193            timestepString = timestepString.replace('+00:00', 'Z')
194
195            wmsRequest = '%s?request=GetMap&SERVICE=%s&FORMAT=%s&LAYERS=%s&BBOX=%s&WIDTH=%s&HEIGHT=%s&CRS=%s&TIME=%s' % (url, serviceVersion, imageFormat, self.layer.name, bBox, imageWidth, imageHeight, crs, timestepString)
196
197            return wmsRequest
198
199        def buildKmlGroundOverlay(timestep):
200            (timespanStart, timespanEnd) = self.getLogicalTimespan(timestep)
201            return KMLGroundOverlay(
202                timestep.isoformat(),
203                buildWMSRequest(timestep),
204                timespanStart, timespanEnd,
205                -180, -90, 180, 90,
206                visible = False
207                )
208
209        # Create a KML folder that represents the view of the layer
210        kmlLayerViewFolder = KMLFolder(self.name, [], visible = False, opened = False, description = self.description)
211
212        # Create a categorisation dictionary, dict, which will contain categories (as returned by
213        # self.getCategory()) as keys, and timesteps belonging into those categories as values.
214        dict = {}
215        for timestep in self.layer.timesteps:
216            category = self.getCategory(timestep)
217            if not dict.has_key(category):
218                dict[category] = []
219            dict[category].append(timestep)
220        categories = dict.keys()
221        categories.sort()
222
223        for category in categories:
224            categoryDescription = self.getCategoryDescription(category)
225            categoryTimesteps = dict[category]
226            kmlCategoryFolder = KMLFolder(categoryDescription, [], visible = False, opened = False)
227            for timestep in categoryTimesteps:
228                kmlGroundOverlay = buildKmlGroundOverlay(timestep)
229                if self.areCategoriesListedExplicitly():
230                    kmlCategoryFolder.children.append(kmlGroundOverlay)
231                else:
232                    kmlLayerViewFolder.children.append(kmlGroundOverlay)
233            if self.areCategoriesListedExplicitly():
234                kmlLayerViewFolder.children.append(kmlCategoryFolder)
235
236        return kmlLayerViewFolder
237
238class ViewWholeTimecourse(View):
239
240    def __init__(self, layer, wmsRequestConfigElement):
241        View.__init__(self, layer, wmsRequestConfigElement)
242        self.name = 'Whole timecourse'
243        self.description = 'All periods as a contiguous animation. Periods are substituted with logical years.<br><br>Because animation in Google Earth cannot skip between dates, logical years are used to keep the animation contiguous, as will be visible on the animation bar. The first period is substituted with year 1, etc.'
244        yearSet = set()
245        for timestep in self.layer.timesteps:
246            yearSet.add(timestep.year)
247        self.sortedYears = list(yearSet); self.sortedYears.sort()
248
249    def areCategoriesListedExplicitly(self):
250        return False
251
252    def getLogicalTimespan(self, timestep):
253        category = self.getCategory(timestep)
254        timespanStart = self._getFirstDayOfMonth( timestep.replace(year = category) )
255        timespanEnd = self._getMonthHence(timespanStart)
256        return (timespanStart, timespanEnd)
257
258    def getCategory(self, timestep):
259        try:
260            return self.sortedYears.index(timestep.year) + 1
261        except ValueError:
262            raise ValueError("Timestep's year is not among years that define the categories.")
263
264    def getCategoryDescription(self, category):
265        '''Get a human-readable description of the category (here, return category verbatim).'''
266        return str(category)
267
268class ViewSplittedByMonth(View):
269
270    def __init__(self, layer, wmsRequestConfigElement):
271        View.__init__(self, layer, wmsRequestConfigElement)
272        self.name = 'Compare months'
273        self.description = 'In each period, the selected month spreads to cover the whole period.<br><br>Note that if duration of each period is shorter than the spacing between the periods (e.g. for some 20 year climatologies), there will be "blind spots" in the animation.'
274
275    def areCategoriesListedExplicitly(self):
276        return True
277
278    def getLogicalTimespan(self, timestep):
279        timespanStart = self._getHalfPeriodEarlier(timestep)
280        timespanEnd = self._getHalfPeriodLater(timestep)
281        return (timespanStart, timespanEnd)
282
283    def getCategory(self, timestep):
284        return timestep.month
285
286    def getCategoryDescription(self, category):
287        '''
288        Get a human-readable description of the category.
289        For instance, for category being 2, the result is 'February'.
290        '''
291        if not ( isinstance(category, int) and category >= 1 and category <= 12 ):
292            raise ValueError('Category not an integer between 1 and 12.')
293        month = category
294        monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 
295                      'July', 'August', 'September', 'October', 'November', 'December']
296        return monthNames[month-1]
297
298class ViewSplittedByPeriod(View):
299
300    def __init__(self, layer, wmsRequestConfigElement):
301        View.__init__(self, layer, wmsRequestConfigElement)
302        self.name = 'Split by period'
303        self.description = 'Animate the selected period only.<br><br>The animation runs only during the period\'s central year, but actually covers the whole period.'
304
305    def areCategoriesListedExplicitly(self):
306        return True
307
308    def getLogicalTimespan(self, timestep):
309        timespanStart = self._getFirstDayOfMonth(timestep)
310        timespanEnd = self._getMonthHence(timespanStart)
311        return (timespanStart, timespanEnd)
312
313    def getCategory(self, timestep):
314        return timestep.year
315
316    def getCategoryDescription(self, category):
317        '''
318        Get a human-readable description of the category that timestep belongs to.
319        For instance, for 1990, the result would be 'Period of 1990'.
320        '''
321        if not isinstance(category, int):
322            raise ValueError('Category not an integer (a year)')
323        year = category
324        return 'Period of ' + str(year)
325
326class WMSCapabilities:
327
328    '''[DOC]'''
329
330    def __init__(self):
331        self.topWmsLayer = None
332
333    def parseXML(self, wmsCapabilitiesElement):
334        topLayerElement = wmsCapabilitiesElement.find('{%s}Capability/{%s}Layer' % (wmsXmlNamespace, wmsXmlNamespace))
335        self.topWmsLayer = wmsLayerFactory(topLayerElement)
336
337    def __repr__(self):
338        if self.topWmsLayer:
339            return '--- WMSCapabilities object with top layer as follows): ' + repr(self.topWmsLayer) + ' ---'
340        else:
341            return '--- WMSCapabilities object with no top layer ---'
342
343class WMSLayersConvertor:
344   
345    def __init__(self, topWmsLayer, wmsRequestConfigElement, baseKmlOutputDirectory, maxDirDepth):
346        self.topWmsLayer = topWmsLayer
347        self.wmsRequestConfigElement = wmsRequestConfigElement
348        self.baseKmlOutputDirectory = baseKmlOutputDirectory
349        self.maxDirDepth = maxDirDepth
350       
351    def convert(self):
352
353        def _convertToKML(wmsLayer, parentLayersStack):
354            '''
355            @param parentLayersStack: an immutable list (acting as a stack) with the WMSLayer objects that have
356            been passed through.
357            @return: a KMLFolder object containing a folder structure (including the backtrack from the parentLayersStack)
358            with the KML representation of wmsLayer at the bottom.
359            '''
360            viewTypes = [ViewWholeTimecourse, ViewSplittedByMonth, ViewSplittedByPeriod]
361            layerKmlFolder = wmsLayer.toKML(self.wmsRequestConfigElement, viewTypes)
362
363            # Backtrack through parentLayersStack in order to include the upper folders in the KML structure as well
364            # (that way it is much easier to see in Google Earth where we are):
365            if len(parentLayersStack) == 0:
366                return layerKmlFolder
367            else:
368                childKmlFolder = layerKmlFolder
369                while len(parentLayersStack) > 0:
370                    stackKmlLayer = parentLayersStack.pop()
371                    kmlFolder = KMLFolder(stackKmlLayer.title, [], visible = False, opened = False)
372                    kmlFolder.children.append(childKmlFolder)
373                    childKmlFolder = kmlFolder
374                return kmlFolder
375
376        def _convertToFile(wmsLayer, parentLayersStack, parentDir):
377
378            # Convert the current layer into a KML document
379            topKmlFolder = _convertToKML(wmsLayer, parentLayersStack)
380
381            # Create a KML document with no styles
382            kmlDocument = KMLDocument(topKmlFolder.name, [])           
383
384            # Replace the uppermost KMLFolder object with a KMLDocument object
385            kmlDocument.elements = topKmlFolder.children
386
387            # Save the KML document
388            dockmlFilename = parentDir + '/doc.kml'
389            kmzFilename = parentDir + '/' + wmsLayer.title + '.kmz'
390            kmlDocument.save(dockmlFilename)
391            quietZipCommand = 'zip -q ' + ' "' + kmzFilename + '" "' + dockmlFilename + '"'
392            os.system(quietZipCommand)
393            os.remove(dockmlFilename)
394            print 'Saved file "%s"' % kmzFilename
395
396        def _convertToDirectory(wmsLayer, parentLayersStack, parentDir, currentLevel):
397            '''recursive'''
398            if currentLevel < self.maxDirDepth and not isinstance(wmsLayer, BottomWMSLayer):
399                currentDir = parentDir + '/' + wmsLayer.title
400                os.mkdir(currentDir)
401                print 'Created directory "%s"' % currentDir
402                for childWmsLayer in wmsLayer.children:
403                    _convertToDirectory(childWmsLayer, parentLayersStack + [wmsLayer], currentDir, currentLevel+1)
404            elif currentLevel == self.maxDirDepth or isinstance(wmsLayer, BottomWMSLayer):
405                _convertToFile(wmsLayer, parentLayersStack, parentDir)
406            else:
407                pass
408
409        _convertToDirectory(self.topWmsLayer, [], self.baseKmlOutputDirectory, 0)
Note: See TracBrowser for help on using the repository browser.