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

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

Changed convertor so that KML files now contain full KML folder hierarchy.

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
127    def areCategoriesListedExplicitly(self):
128        '''
129        @returns: A boolean value that signifies whether the self.toKML() method should list the categories
130                  explicitly (in separate KMLFolder's). Must be implemented by all derived classes.
131        '''
132        raise NotImplementedError()
133
134    def getLogicalTimespan(self, timestep):
135        '''
136        Abstract method, defined in derived classes.
137        Translates a single time step into a time span.
138        @param timestep: The date step (a datetime object)
139        @return: The (timespanStart, timespanEnd) tuple (both are datetime objects)
140        '''
141        pass
142
143    def getCategory(self, timestep):
144        pass
145
146    def getCategoryDescription(self, category):
147        '''Abstract method, defined in derived classes. Get a human-readable description of the category.'''
148        pass
149
150    def _getSameDate(self, timestep):
151        return timestep
152
153    def _getFirstDayOfMonth(self, timestep):
154        return timestep.replace(day=1)
155
156    def _getMonthHence(self, timestep):
157        if timestep.month+1 <= 12:
158            return timestep.replace(month=timestep.month+1)
159        else:
160            return timestep.replace(year=timestep.year+1, month=1)
161   
162    def _getYearHence(self, timestep):
163        return timestep.replace(year=timestep.year+1)
164
165    def _getHalfPeriodEarlier(self, timestep):
166        return timestep.replace(year = timestep.year-self.layer.getPeriod()/2)
167
168    def _getHalfPeriodLater(self, timestep):
169        return timestep.replace(year = timestep.year+self.layer.getPeriod()/2)
170
171    def toKML(self):
172
173        def buildWMSRequest(timestep):
174            ''' Build a WMS request '''
175
176            # We will be using configuration for WMS request
177            c = self.wmsRequestConfigElement
178
179            # Set request configuration parameters
180            url = c.find('URL').text
181            serviceVersion = c.find('ServiceVersion').text
182            imageFormat = c.find('ImageFormat').text
183            imageWidth = c.find('ImageWidth').text
184            imageHeight = c.find('ImageHeight').text
185            crs = c.find('CRS').text
186
187            bBox = '-180,-90,180,90'
188
189            # If the timezone is UTC (which in ISO form would look like 'yyyy-mm-ddThh:mm:ss+00:00'),
190            # then replace it with 'Z'.
191            timestepString = timestep.isoformat()
192            timestepString = timestepString.replace('+00:00', 'Z')
193
194            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)
195
196            return wmsRequest
197
198        def buildKmlGroundOverlay(timestep):
199            (timespanStart, timespanEnd) = self.getLogicalTimespan(timestep)
200            return KMLGroundOverlay(
201                timestep.isoformat(),
202                buildWMSRequest(timestep),
203                timespanStart, timespanEnd,
204                -180, -90, 180, 90,
205                visible = False
206                )
207
208        # Create a KML folder that represents the view of the layer
209        kmlLayerViewFolder = KMLFolder(self.name, [], visible = False, opened = False)
210
211        # Create a categorisation dictionary, dict, which will contain categories (as returned by
212        # self.getCategory()) as keys, and timesteps belonging into those categories as values.
213        dict = {}
214        for timestep in self.layer.timesteps:
215            category = self.getCategory(timestep)
216            if not dict.has_key(category):
217                dict[category] = []
218            dict[category].append(timestep)
219        categories = dict.keys()
220        categories.sort()
221
222        for category in categories:
223            categoryDescription = self.getCategoryDescription(category)
224            categoryTimesteps = dict[category]
225            kmlCategoryFolder = KMLFolder(categoryDescription, [], visible = False, opened = False)
226            for timestep in categoryTimesteps:
227                kmlGroundOverlay = buildKmlGroundOverlay(timestep)
228                if self.areCategoriesListedExplicitly():
229                    kmlCategoryFolder.children.append(kmlGroundOverlay)
230                else:
231                    kmlLayerViewFolder.children.append(kmlGroundOverlay)
232            if self.areCategoriesListedExplicitly():
233                kmlLayerViewFolder.children.append(kmlCategoryFolder)
234
235        return kmlLayerViewFolder
236
237class ViewWholeTimecourse(View):
238
239    def __init__(self, layer, wmsRequestConfigElement):
240        self.name = 'Whole timecourse'
241        View.__init__(self, layer, wmsRequestConfigElement)
242        yearSet = set()
243        for timestep in self.layer.timesteps:
244            yearSet.add(timestep.year)
245        self.sortedYears = list(yearSet); self.sortedYears.sort()
246
247    def areCategoriesListedExplicitly(self):
248        return False
249
250    def getLogicalTimespan(self, timestep):
251        category = self.getCategory(timestep)
252        timespanStart = self._getFirstDayOfMonth( timestep.replace(year = category) )
253        timespanEnd = self._getMonthHence(timespanStart)
254        return (timespanStart, timespanEnd)
255
256    def getCategory(self, timestep):
257        try:
258            return self.sortedYears.index(timestep.year) + 1
259        except ValueError:
260            raise ValueError("Timestep's year is not among years that define the categories.")
261
262    def getCategoryDescription(self, category):
263        '''Get a human-readable description of the category (here, return category verbatim).'''
264        return str(category)
265
266class ViewSplittedByMonth(View):
267
268    def __init__(self, layer, wmsRequestConfigElement):
269        self.name = 'Compare months'
270        View.__init__(self, layer, wmsRequestConfigElement)
271
272    def areCategoriesListedExplicitly(self):
273        return True
274
275    def getLogicalTimespan(self, timestep):
276        timespanStart = self._getHalfPeriodEarlier(timestep)
277        timespanEnd = self._getHalfPeriodLater(timestep)
278        return (timespanStart, timespanEnd)
279
280    def getCategory(self, timestep):
281        return timestep.month
282
283    def getCategoryDescription(self, category):
284        '''
285        Get a human-readable description of the category.
286        For instance, for category being 2, the result is 'February'.
287        '''
288        if not ( isinstance(category, int) and category >= 1 and category <= 12 ):
289            raise ValueError('Category not an integer between 1 and 12.')
290        month = category
291        monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 
292                      'July', 'August', 'September', 'October', 'November', 'December']
293        return monthNames[month-1]
294
295class ViewSplittedByPeriod(View):
296
297    def __init__(self, layer, wmsRequestConfigElement):
298        self.name = 'Compare period'
299        View.__init__(self, layer, wmsRequestConfigElement)
300
301    def areCategoriesListedExplicitly(self):
302        return True
303
304    def getLogicalTimespan(self, timestep):
305        timespanStart = self._getFirstDayOfMonth(timestep)
306        timespanEnd = self._getMonthHence(timespanStart)
307        return (timespanStart, timespanEnd)
308
309    def getCategory(self, timestep):
310        return timestep.year
311
312    def getCategoryDescription(self, category):
313        '''
314        Get a human-readable description of the category that timestep belongs to.
315        For instance, for 1990, the result would be 'Period of 1990'.
316        '''
317        if not isinstance(category, int):
318            raise ValueError('Category not an integer (a year)')
319        year = category
320        return 'Period of ' + str(year)
321
322class WMSCapabilities:
323
324    '''[DOC]'''
325
326    def __init__(self):
327        self.topWmsLayer = None
328
329    def parseXML(self, wmsCapabilitiesElement):
330        topLayerElement = wmsCapabilitiesElement.find('{%s}Capability/{%s}Layer' % (wmsXmlNamespace, wmsXmlNamespace))
331        self.topWmsLayer = wmsLayerFactory(topLayerElement)
332
333    def __repr__(self):
334        if self.topWmsLayer:
335            return '--- WMSCapabilities object with top layer as follows): ' + repr(self.topWmsLayer) + ' ---'
336        else:
337            return '--- WMSCapabilities object with no top layer ---'
338
339class WMSLayersConvertor:
340   
341    def __init__(self, topWmsLayer, wmsRequestConfigElement, baseKmlOutputDirectory, maxDirDepth):
342        self.topWmsLayer = topWmsLayer
343        self.wmsRequestConfigElement = wmsRequestConfigElement
344        self.baseKmlOutputDirectory = baseKmlOutputDirectory
345        self.maxDirDepth = maxDirDepth
346       
347    def convert(self):
348
349        def _convertToKML(wmsLayer, parentLayersStack):
350            '''
351            @param parentLayersStack: an immutable list (acting as a stack) with the WMSLayer objects that have
352            been passed through.
353            @return: a KMLFolder object containing a folder structure (including the backtrack from the parentLayersStack)
354            with the KML representation of wmsLayer at the bottom.
355            '''
356            viewTypes = [ViewWholeTimecourse, ViewSplittedByMonth, ViewSplittedByPeriod]
357            layerKmlFolder = wmsLayer.toKML(self.wmsRequestConfigElement, viewTypes)
358
359            # Backtrack through parentLayersStack in order to include the upper folders in the KML structure as well
360            # (that way it is much easier to see in Google Earth where we are):
361            if len(parentLayersStack) == 0:
362                return layerKmlFolder
363            else:
364                childKmlFolder = layerKmlFolder
365                while len(parentLayersStack) > 0:
366                    stackKmlLayer = parentLayersStack.pop()
367                    kmlFolder = KMLFolder(stackKmlLayer.title, [], visible = False, opened = False)
368                    kmlFolder.children.append(childKmlFolder)
369                    childKmlFolder = kmlFolder
370                return kmlFolder
371
372        def _convertToFile(wmsLayer, parentLayersStack, parentDir):
373
374            # Convert the current layer into a KML document
375            topKmlFolder = _convertToKML(wmsLayer, parentLayersStack)
376
377            # Create a KML document with no styles
378            kmlDocument = KMLDocument(topKmlFolder.name, [])           
379
380            # Replace the uppermost KMLFolder object with a KMLDocument object
381            kmlDocument.elements = topKmlFolder.children
382
383            # Save the KML document
384            filename = parentDir + '/' + wmsLayer.title + '.kml'
385            kmlDocument.save(filename)
386            print 'Saved file "%s"' % filename
387
388        def _convertToDirectory(wmsLayer, parentLayersStack, parentDir, currentLevel):
389            '''recursive'''
390            if currentLevel < self.maxDirDepth and not isinstance(wmsLayer, BottomWMSLayer):
391                currentDir = parentDir + '/' + wmsLayer.title
392                os.mkdir(currentDir)
393                print 'Created directory "%s"' % currentDir
394                for childWmsLayer in wmsLayer.children:
395                    _convertToDirectory(childWmsLayer, parentLayersStack + [wmsLayer], currentDir, currentLevel+1)
396            elif currentLevel == self.maxDirDepth or isinstance(wmsLayer, BottomWMSLayer):
397                _convertToFile(wmsLayer, parentLayersStack, parentDir)
398            else:
399                pass
400
401        _convertToDirectory(self.topWmsLayer, [], self.baseKmlOutputDirectory, 0)
Note: See TracBrowser for help on using the repository browser.