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

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

Replaced "pylab" with "matplotlib" in imports in csml2kml, to make it work on superglue.

Line 
1'''
2Classes for work with I{<wms:Layer>}'s.
3'''
4
5import os
6import re
7from matplotlib import dates     # a very good date/time module from matplotviz -- allows years < 1970
8from KML import *
9
10wmsXmlNamespace = 'http://www.opengis.net/wms'  # a XML namespace in which the <wms:Layer> element is defined
11
12class BBox:
13    '''
14    A geographic rectangular bounding box, bounding by longitude and latitude (but not altitude).
15    '''
16    def __init__(self, west, south, east, north):
17        '''
18        @type west: int
19        @type south: int
20        @type east: int
21        @type north: int
22        '''
23        self.west = west; self.east = east
24        self.south = south; self.north = north
25    def __repr__(self):
26        '''
27        Print out the bounding box in a WMS query format (e.g.: '-180,-90,180,90')
28        '''
29        return str(self.west) + ',' + str(self.south) + ',' + str(self.east) + ',' + str(self.north)
30
31def wmsLayerFactory(layerElement, parentBBox = None):
32    '''
33    A factory function for generating new WMSLayer objects.
34    @param layerElement: A I{<wms:Layer>} element containing the top layer of a hierarchy of WMS layers.
35    @type layerElement: C{cElementTree.Element}
36    @param parentBBox: Bounding box of the parent layer. Leave C{None} (used by the function in recursive calls).
37    @type parentBBox: C{BBox}
38    '''
39    name = layerElement.find('{%s}Name' % wmsXmlNamespace).text
40    title = layerElement.find('{%s}Title' % wmsXmlNamespace).text
41    abstract = layerElement.find('{%s}Abstract' % wmsXmlNamespace).text
42
43    bboxElement = layerElement.find('{%s}BoundingBox' % wmsXmlNamespace)
44    if not bboxElement == None:
45        bboxWest  = float(bboxElement.get('minx'))
46        bboxEast  = float(bboxElement.get('maxx'))
47        bboxSouth = float(bboxElement.get('miny'))
48        bboxNorth = float(bboxElement.get('maxy'))
49        bbox = BBox(bboxWest, bboxSouth, bboxEast, bboxNorth)
50    elif parentBBox:
51        bbox = parentBBox
52    else:
53        raise AttributeError('Layer has no own nor parental bounding box')
54       
55    childElements = layerElement.findall('{%s}Layer' % wmsXmlNamespace)
56    childWmsLayers = []
57    for childElement in childElements:
58        childWmsLayer = wmsLayerFactory(childElement, parentBBox = bbox)
59        childWmsLayers.append(childWmsLayer)
60    if childElements != []:
61        return WMSLayer(name, title, abstract, bbox, childWmsLayers)       
62    else:
63        dimensionElements = layerElement.findall('{%s}Dimension' % wmsXmlNamespace)
64        for dimensionElement in dimensionElements:
65            if dimensionElement.get('name') == 'time':
66                timesteps = map( dates.dateutil.parser.parse, dimensionElement.text.split(',') )
67        return BottomWMSLayer(name, title, abstract, bbox, timesteps)
68
69class WMSLayer:
70    '''
71    A representation of the I{<wms:Layer>} element, which is normally contained within a I{<wms:Capabilities>} element
72    (see C{WMSCapabilities}).
73    However, bottom-layer I{<wms:Layer>} elements are represented by the C{BottomWMSLayer} objects,
74    with overriden behaviour.
75    @ivar name: Name of the layer
76    @ivar title: Title of the layer (more human readable than name)
77    @ivar abstract: Abstract of the layer (explanation of underlying data)
78    @ivar bbox: A C{BBox} of the layer
79    @ivar childen: A list of C{WMSLayer} objects contained within the WMS layer
80    '''
81
82    def __init__(self, name, title, abstract, bbox, children):
83        self.name = name
84        self.title = title
85        self.abstract = abstract
86        self.bbox = bbox
87        self.children = children
88
89    def __repr__(self):
90        return str(vars(self))
91
92    def toKML(self, wmsRequestConfigElement, viewTypes, parentDir, parentDirUrl):
93        '''
94        Export the non-bottom layer to KML. This creates a hierarchy of KMZ files embedded in directories.
95        Each KMZ files encodes a single I{<wms:Layer>} element, as follows:
96          - Each KMZ file encoding a {non-bottom WMS layer} contains a number of I{<kml:NetworkLink>}'s mapping
97            to the KMZ files in a lower directory.
98            The I{<kml:NetworkLink>} contains an absolute URL of the target on the machine from the KMZ file
99            is being served.
100          - Each KML file encoding a I{bottom WMS layer} contains views of the data in the bottom layer,
101            with the views specified by the C{viewTypes} parameter.
102        @param wmsRequestConfigElement: The I{<WMSRequest>} element from the config file (containing information
103        about how WMS requests are to be made, e.g. which server to use, what resolution to use, etc.)
104        @type wmsRequestConfigElement: C{cElementTree.Element}
105        @param viewTypes: A list of objects descended from C{View}, which determine how the visible layer data is to be
106        viewed.
107        @type viewTypes: C{View} list
108        @param parentDir: A directory to be used as the root of the output. The KMZ file corresponding to the uppermost
109        layer will be contained directly in this directory.
110        @type parentDir: C{str}
111        @param parentDirUrl: The URL from which the output will be served, corresponding to the root output directory.
112        @type parentDirUrl: C{str}
113        @return: An object representing a I{<kml:NetworkLink>} to the uppermost-leve created KMZ file
114        @rtype: C{KML.KMLNetworkLink}
115        '''
116
117        # Create an "underscored" version of the title, in which all spaces, slashes, and backslashes
118        # are replaced with underscores.
119        title_ = self.title.replace(' ', '_').replace('/', '_').replace('\\', '_')
120
121        # Determine a full directory path and filename of the resulting KMZ file. Also determine thier corresponding URLs.
122        dir = parentDir + '/' + title_
123        dirUrl = parentDirUrl + '/' + title_
124        filename = dir + '.kmz'
125        fileUrl = dirUrl + '.kmz'
126
127        # Create the target directory.
128        os.mkdir(dir)
129        print 'Created directory "%s".' % dir
130
131        # Create a representation of an empty KML document.
132        kmlDocument = KMLDocument(self.title, [])
133
134        # Add each embedded layer into the document.
135        for childWmsLayer in self.children:
136            kmlDocument.elements.append(
137                childWmsLayer.toKML(wmsRequestConfigElement, viewTypes, dir, dirUrl)
138                )
139
140        # Save the document into the KMZ file (this performs ZIP compression automatically).
141        kmlDocument.save(filename)
142        print 'Saved file "%s".' % filename
143   
144        # Return a network link that links to the KMZ file just created.
145        return KMLNetworkLink(self.title, fileUrl, description = self.abstract, visible = False)
146
147class BottomWMSLayer(WMSLayer):
148
149    '''
150    Represents a bottom-level WMS layer (i.e. with no embedded sub-layers, and a I{<wms:Dimension>} element).
151    @ivar name: Name of the layer
152    @ivar title: Title of the layer (more human readable than name)
153    @ivar abstract: Abstract of the layer (explanation of underlying data)
154    @ivar bbox: C{BBox}
155    @ivar timesteps: A list of C{matplotlib.dates.datetime.datetime} objects (the time dimension of the layer)
156    '''
157   
158    def __init__(self, name, title, abstract, bbox, timesteps):
159
160        self.name = name
161        self.title = title
162        self.abstract = abstract
163        self.bbox = bbox
164        # but no self.children
165        self.timesteps = timesteps
166
167    def _parseName(self):
168        '''
169        Parse name of the layer I{name} element, and extract various layer parameters from it.
170        '''
171        mo = re.match('(.+)\:(.+)\:(.+)', self.name)
172        if mo:
173            (modelName, scenarioName, rest) = mo.groups()
174        else:
175            (modelName, scenarioName, rest) = (None, None, self.name)
176
177        mo2 = re.match('(clim|change)\_(\d+)\/(.+)', rest)
178        if mo2:
179            (type, periodText, description) = mo2.groups()
180            period = int(periodText)
181        else:
182            raise ValueError('Cannot parse in layer name')
183
184        return (type, period, description, modelName, scenarioName)
185
186    def getType(self):
187        '''@return: Layer type ("climatology" or "changes")'''
188        return self._parseName()[0]
189
190    def getPeriod(self):
191        '''@return: The period length (int)'''
192        return self._parseName()[1]
193   
194    def getDescription(self):
195        return self._parseName()[2]
196
197    def getModelName(self):
198        return self._parseName()[3]
199
200    def getScenarioName(self):
201        return self._parseName()[4]
202
203    def toKML(self, wmsRequestConfigElement, viewTypes, parentDir, parentDirUrl):
204        '''
205        Overrides the behaviour of C{WMSLayer.toKML}. Returns a C{KML.KMLFolder} object which represents
206        various view of the bottom layer, as per the C{viewTypes} parameter.
207        @param wmsRequestConfigElement: The <WMSRequest> element from the config file (containing information
208        about how WMS requests are to be made, e.g. which server to use, what resolution to use, etc.)
209        @type wmsRequestConfigElement: C{cElementTree.Element}
210        @param viewTypes: Determine in what ways the visible layer data is to be viewed.
211        @type viewTypes: A list of C{View} classes (note: classes, not instances!)
212        @param parentDir: (ignored)
213        layer will be contained directly in this directory.
214        @type parentDir: C{str}
215        @param parentDirUrl: (ignored)
216        @type parentDirUrl: C{str}
217        @return: An object containing individual views of the layers (as sub-folders).
218        @rtype: C{KML.KMLFolder}
219        '''
220       
221        # For each viewType, generate a new View object, that uses this layer (self) as a model and uses
222        # WMS request configuration wmsRequestConfigElement. The use that object to generate KML for this layer.
223        kmlLayerFolder = KMLFolder(self.title, [], visible = False, opened = False)
224        for viewType in viewTypes:
225            view = viewType(self, wmsRequestConfigElement)
226            kmlLayerFolder.children.append( view.toKML() )
227        return kmlLayerFolder
228
229class View:
230    '''
231    A view of a C{BottomWMSLayer} (as in "model-view-controller", an instance of this class is a view,
232    which the layer is the model). It determines how BottomWMSLayer data can be visualised in KML.
233    In particular, it defines logical transforms of time-points into time-spans.
234    @ivar layer: The layer being viewed.
235    @ivar wmsRequestConfigElement: A config element that defines format of WMS requests.
236    '''
237
238    def __init__(self, layer, wmsRequestConfigElement):
239        '''
240        Initialize the view.
241        @param layer: Some views (not all) may need to "see" the layer data (although some ignore it).
242        @type layer: C{BottomWMSLayer}
243        '''
244        self.layer = layer
245        self.wmsRequestConfigElement = wmsRequestConfigElement
246        self.description = None
247
248    def areCategoriesListedExplicitly(self):
249        '''
250        @returns: A boolean value that signifies whether the self.toKML() method should list the categories
251                  explicitly (in separate KMLFolder's). Must be implemented by all derived classes.
252        '''
253        raise NotImplementedError()
254
255    def getLogicalTimespan(self, timestep):
256        '''
257        Translate a single time-step into a time-span.
258        @param timestep: The date/time step
259        @type timestep: C{matplotlib.dates.datetime.datetime}
260        @return: The (timespanStart, timespanEnd) tuple (both are datetime objects)
261        '''
262        pass
263
264    def getCategory(self, timestep):
265        '''
266        Get a category in which a timestep belongs to.
267        @param timestep: The timestep.
268        @type timestep: C{matplotlib.dates.datetime.datetime}
269        @return: The category in which C{timestep} belongs to
270        '''
271        pass
272
273    def getCategoryDescription(self, category):
274        '''
275        Get a human-readable description of a category.
276        @param category: The category
277        @type category: of undefined type, which depends on category
278        @return: A string describing the category (used for naming the category's KML folder, if any).
279        '''
280        pass
281
282    def _getSameDate(self, timestep):
283        '''
284        A time-step transform, used by derived classes.
285        @type timestep: C{matplotlib.dates.datetime.datetime}
286        '''
287        return timestep
288
289    def _getFirstDayOfMonth(self, timestep):
290        '''
291        A time-step transform, used by derived classes.
292        @type timestep: C{matplotlib.dates.datetime.datetime}
293        '''
294        return timestep.replace(day=1)
295
296    def _getMonthHence(self, timestep):
297        '''
298        A time-step transform, used by derived classes.
299        @type timestep: C{matplotlib.dates.datetime.datetime}
300        '''
301        if timestep.month+1 <= 12:
302            return timestep.replace(month=timestep.month+1)
303        else:
304            return timestep.replace(year=timestep.year+1, month=1)
305   
306    def _getYearHence(self, timestep):
307        '''
308        A time-step transform, used by derived classes.
309        @type timestep: C{matplotlib.dates.datetime.datetime}
310        '''
311        return timestep.replace(year=timestep.year+1)
312
313    def _getHalfPeriodEarlier(self, timestep):
314        '''
315        A time-step transform, used by derived classes.
316        @type timestep: C{matplotlib.dates.datetime.datetime}
317        @return: A timestep that is half of the viewed layer's period before C{timestep}.
318        '''
319        return timestep.replace(year = timestep.year-self.layer.getPeriod()/2)
320
321    def _getHalfPeriodLater(self, timestep):
322        '''
323        A time-step transform, used by derived classes.
324        @type timestep: C{matplotlib.dates.datetime.datetime}
325        @return: A timestep that is half of the viewed layer's period after C{timestep}.
326        '''
327        return timestep.replace(year = timestep.year+self.layer.getPeriod()/2)
328
329    def toKML(self):
330        '''
331        Get a KML representation of the layer C{self.layer} in this view.
332        '''
333
334        def buildWMSRequest(timestep):
335            ''' Build a WMS request for retrieving this timestep, using self.wmsRequestConfigElement.'''
336
337            # We will be using configuration for WMS request
338            c = self.wmsRequestConfigElement
339
340            # Set request configuration parameters
341            url = c.find('URL').text
342            serviceVersion = c.find('ServiceVersion').text
343            imageFormat = c.find('ImageFormat').text
344            imageWidth = c.find('ImageWidth').text
345            imageHeight = c.find('ImageHeight').text
346            crs = c.find('CRS').text
347
348            # If the timezone is UTC (which in ISO form would look like 'yyyy-mm-ddThh:mm:ss+00:00'),
349            # then replace it with 'Z'.
350            timestepString = timestep.isoformat()
351            timestepString = timestepString.replace('+00:00', 'Z')
352
353            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)
354
355            return wmsRequest
356
357        def buildKmlGroundOverlay(timestep):
358            '''@return: A C{KML.KMLGroundOverlay} object representing a ground overlay for this timestep.'''
359
360            # Find the time-span which corresponds to this time-step in this view.
361            (timespanStart, timespanEnd) = self.getLogicalTimespan(timestep)
362
363            # Return a ground overlay element which is valid for the given time-span and contains a dynamic hyperlink
364            # to the WMS service.
365            return KMLGroundOverlay(
366                timestep.isoformat(),
367                buildWMSRequest(timestep),
368                timespanStart, timespanEnd,
369                self.layer.bbox.west, self.layer.bbox.south, self.layer.bbox.east, self.layer.bbox.north,
370                visible = False
371                )
372
373        # Create a KML folder that represents the view of the layer
374        kmlLayerViewFolder = KMLFolder(self.name, [], visible = False, opened = False, description = self.description)
375
376        # Create a categorisation dictionary, dict, which will contain categories (as returned by
377        # self.getCategory()) as keys, and timesteps belonging into those categories as values.
378        dict = {}
379        for timestep in self.layer.timesteps:
380            category = self.getCategory(timestep)
381            if not dict.has_key(category):
382                dict[category] = []
383            dict[category].append(timestep)
384        categories = dict.keys()
385        categories.sort()
386
387        # Iterate through categories, creating a special folder for each, and putting the ground overlays
388        # corresponding to that category into that folder. However, if creating special directories for
389        # individual categories is not permitted in this view (self.areCategoriesListedExplicitly() == False),
390        # then place the ground overlays directly into the layer view folder.
391        for category in categories:
392            categoryDescription = self.getCategoryDescription(category)
393            categoryTimesteps = dict[category]
394            kmlCategoryFolder = KMLFolder(categoryDescription, [], visible = False, opened = False)
395            for timestep in categoryTimesteps:
396                kmlGroundOverlay = buildKmlGroundOverlay(timestep)
397                if self.areCategoriesListedExplicitly():
398                    kmlCategoryFolder.children.append(kmlGroundOverlay)
399                else:
400                    kmlLayerViewFolder.children.append(kmlGroundOverlay)
401            if self.areCategoriesListedExplicitly():
402                kmlLayerViewFolder.children.append(kmlCategoryFolder)
403
404        return kmlLayerViewFolder
405
406class ViewWholeTimecourse(View):
407    '''
408    View all periods in one contiguous animation. Layer periods are substituted with logical years
409    (the first period is substituted with year 1, etc.)
410    '''
411
412    def __init__(self, layer, wmsRequestConfigElement):
413        View.__init__(self, layer, wmsRequestConfigElement)
414        self.name = 'Whole timecourse'
415        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.'
416        yearSet = set()
417        for timestep in self.layer.timesteps:
418            yearSet.add(timestep.year)
419        self.sortedYears = list(yearSet); self.sortedYears.sort()
420
421    def areCategoriesListedExplicitly(self):
422        '''@return: C{False}'''
423        return False
424
425    def getLogicalTimespan(self, timestep):
426        '''@return: The date's whole month, placed into the date's logical year.'''
427        category = self.getCategory(timestep)
428        timespanStart = self._getFirstDayOfMonth( timestep.replace(year = category) )
429        timespanEnd = self._getMonthHence(timespanStart)
430        return (timespanStart, timespanEnd)
431
432    def getCategory(self, timestep):
433        '''@return: The logical year (the year's order in the sequence of the layer's years).'''
434        try:
435            return self.sortedYears.index(timestep.year) + 1
436        except ValueError:
437            raise ValueError("Timestep's year is not among years that define the categories.")
438
439    def getCategoryDescription(self, category):
440        '''@return: Return the category verbatim.'''
441        return str(category)
442
443class ViewSplittedByMonth(View):
444    '''
445    In each period, the selected month spreads to cover the whole period.
446    Note that if duration of each period is shorter than the spacing between the periods
447    (e.g. for some 20 year climatologies), there will be "blind spots" in the animation.
448    '''
449
450    def __init__(self, layer, wmsRequestConfigElement):
451        View.__init__(self, layer, wmsRequestConfigElement)
452        self.name = 'Compare months'
453        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.'
454
455    def areCategoriesListedExplicitly(self):
456        '''@return: C{True}'''
457        return True
458
459    def getLogicalTimespan(self, timestep):
460        '''@return: The period which C{timestep} is the center of.'''
461        timespanStart = self._getHalfPeriodEarlier(timestep)
462        timespanEnd = self._getHalfPeriodLater(timestep)
463        return (timespanStart, timespanEnd)
464
465    def getCategory(self, timestep):
466        '''@return: The date's month.'''
467        return timestep.month
468
469    def getCategoryDescription(self, category):
470        '''
471        @type category: int
472        @return: The written form name of the month (for C{category} being 2, the result is 'February').
473        '''
474        if not ( isinstance(category, int) and category >= 1 and category <= 12 ):
475            raise ValueError('Category not an integer between 1 and 12.')
476        month = category
477        monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 
478                      'July', 'August', 'September', 'October', 'November', 'December']
479        return monthNames[month-1]
480
481class ViewSplittedByPeriod(View):
482    '''
483    Animate the selected period only.
484    The animation runs only during the period\'s central year, but actually covers the whole period.
485    '''
486
487    def __init__(self, layer, wmsRequestConfigElement):
488        View.__init__(self, layer, wmsRequestConfigElement)
489        self.name = 'Split by period'
490        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.'
491
492    def areCategoriesListedExplicitly(self):
493        '''@return: C{True}'''
494        return True
495
496    def getLogicalTimespan(self, timestep):
497        '''@return: The date's whole month.'''
498        timespanStart = self._getFirstDayOfMonth(timestep)
499        timespanEnd = self._getMonthHence(timespanStart)
500        return (timespanStart, timespanEnd)
501
502    def getCategory(self, timestep):
503        '''@return: The date's year.'''
504        return timestep.year
505
506    def getCategoryDescription(self, category):
507        '''
508        @type category: int
509        @returns: For instance, for 1990, the result would be 'Period of 1990'.
510        '''
511        if not isinstance(category, int):
512            raise ValueError('Category not an integer (a year)')
513        year = category
514        return 'Period of ' + str(year)
515
516class WMSCapabilities:
517
518    '''
519    A representation of the I{<wms:Capabilities>} element (which gets returned from WMS GetCapability() calls).
520    @ivar topWmsLayer: The top layer of the WMS layer hierarchy.
521    @type topWmsLayer: C{WMSLayer}
522    '''
523
524    def __init__(self):
525        '''Create an empty object.'''
526        self.topWmsLayer = None
527
528    def parseXML(self, wmsCapabilitiesElement):
529        '''
530        Parse in the I{<wms:Capabilities>} element.
531        @param wmsCapabilitiesElement: The element.
532        @type wmsCapabilitiesElement: C{cElementTree.Element}
533        '''
534        topLayerElement = wmsCapabilitiesElement.find('{%s}Capability/{%s}Layer' % (wmsXmlNamespace, wmsXmlNamespace))
535        self.topWmsLayer = wmsLayerFactory(topLayerElement)
536
537    def __repr__(self):
538        if self.topWmsLayer:
539            return '--- WMSCapabilities object with top layer as follows): ' + repr(self.topWmsLayer) + ' ---'
540        else:
541            return '--- WMSCapabilities object with no top layer ---'
Note: See TracBrowser for help on using the repository browser.