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

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

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