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

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

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