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

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

Passed testing using testWMSCapabilities.py for the IPCC "obs" dataset.

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):
350            viewTypes = [ViewWholeTimecourse, ViewSplittedByMonth, ViewSplittedByPeriod]
351            return wmsLayer.toKML(self.wmsRequestConfigElement, viewTypes)
352
353        def _convertToFile(wmsLayer, parentDir):
354            kmlDocument = KMLDocument(wmsLayer.title, [])            # Create a KML document with no styles
355            kmlDocument.elements = _convertToKML(wmsLayer).children
356            filename = parentDir + '/' + wmsLayer.title + '.kml'
357            kmlDocument.save(filename)
358            print 'Saved file "%s"' % filename
359
360        def _convertToDirectory(wmsLayer, parentDir, currentLevel):
361            '''recursive'''
362            if currentLevel < self.maxDirDepth and not isinstance(wmsLayer, BottomWMSLayer):
363                currentDir = parentDir + '/' + wmsLayer.title
364                os.mkdir(currentDir)
365                print 'Created directory "%s"' % currentDir
366                for childWmsLayer in wmsLayer.children:
367                    _convertToDirectory(childWmsLayer, currentDir, currentLevel+1)
368            elif currentLevel == self.maxDirDepth or isinstance(wmsLayer, BottomWMSLayer):
369                _convertToFile(wmsLayer, parentDir)
370            else:
371                pass
372
373        _convertToDirectory(self.topWmsLayer, self.baseKmlOutputDirectory, 0)
Note: See TracBrowser for help on using the repository browser.