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

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

Wrote code for generating a hierarchy of KML files (with kml:NetworkLinks) and directories.

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