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

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

Made list of views views to be used in wms2kml configurable. Enforced timestamps in WMS queries to contain ".0" for microseconds.

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