source: DPPP/kml/csml2kml/python/csml2kml/csml2kml/WMSLayer.py @ 3595

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/DPPP/kml/csml2kml/python/csml2kml/csml2kml/WMSLayer.py@3595
Revision 3595, 14.5 KB checked in by mkochan, 14 years ago (diff)

Split WMSLayer-related functionality into two modules: WMSLayer.py and WMSLayerConvertor.py.

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