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

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

Added comments to 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 parseXML(self, layerElement):
89    #    '''
90    #    Deprecated.
91    #    '''
92    #    raise NotImplementedError('Use the wmsLayerFactory() function instead.')
93
94    def toKML(self, wmsRequestConfigElement, viewTypes, parentDir, parentDirUrl):
95        '''
96        Export the non-bottom layer to KML. This creates a hierarchy of KMZ files embedded in directories.
97        Each KMZ files encodes a single I{<wms:Layer>} element, as follows:
98          - Each KMZ file encoding a {non-bottom WMS layer} contains a number of I{<kml:NetworkLink>}'s mapping
99            to the KMZ files in a lower directory.
100            The I{<kml:NetworkLink>} contains an absolute URL of the target on the machine from the KMZ file
101            is being served.
102          - Each KML file encoding a I{bottom WMS layer} contains views of the data in the bottom layer,
103            with the views specified by the C{viewTypes} parameter.
104        @param wmsRequestConfigElement: The <WMSRequest> element from the config file (containing information
105        about how WMS requests are to be made, e.g. which server to use, what resolution to use, etc.)
106        @type wmsRequestConfigElement: C{cElementTree.Element}
107        @param viewTypes: A list of objects descended from C{View}, which determine how the visible layer data is to be
108        viewed.
109        @type viewTypes: C{View} list
110        @param parentDir: A directory to be used as the root of the output. The KMZ file corresponding to the uppermost
111        layer will be contained directly in this directory.
112        @type parentDir: C{str}
113        @param parentDirUrl: The URL from which the output will be served, corresponding to the root output directory.
114        @type parentDirUrl: C{str}
115        @return: A C{KML.KMLNetworkLink} object representing a I{<kml:NetworkLink>} to the uppermost-leve created KMZ file
116        '''
117
118        # Create an "underscored" version of the title, in which all spaces, slashes, and backslashes
119        # are replaced with underscores.
120        title_ = self.title.replace(' ', '_').replace('/', '_').replace('\\', '_')
121
122        # Determine a full directory path and filename of the resulting KMZ file. Also determine thier corresponding URLs.
123        dir = parentDir + '/' + title_
124        dirUrl = parentDirUrl + '/' + title_
125        filename = dir + '.kmz'
126        fileUrl = dirUrl + '.kmz'
127
128        # Create the target directory.
129        os.mkdir(dir)
130        print 'Created directory "%s".' % dir
131
132        # Create a representation of an empty KML document.
133        kmlDocument = KMLDocument(self.title, [])
134
135        # Add each embedded layer into the document.
136        for childWmsLayer in self.children:
137            kmlDocument.elements.append(
138                childWmsLayer.toKML(wmsRequestConfigElement, viewTypes, dir, dirUrl)
139                )
140
141        # Save the document into the KMZ file (this performs ZIP compression automatically).
142        kmlDocument.save(filename)
143        print 'Saved file "%s".' % filename
144   
145        # Return a network link that links to the KMZ file just created.
146        return KMLNetworkLink(self.title, fileUrl, description = self.abstract, visible = False)
147
148class BottomWMSLayer(WMSLayer):
149
150    '''
151    Represents a bottom-level WMS layer (i.e. with no embedded sub-layers and a I{<wms:Dimension>} element.
152    @ivar name: Name of the layer
153    @ivar title: Title of the layer (more human readable than name)
154    @ivar abstract: Abstract of the layer (explanation of underlying data)
155    @ivar bbox: C{BBox}
156    @ivar timesteps: A list of C{pylab.dates.datetime.datetime} objects (the time dimension of the layer)
157    '''
158   
159    def __init__(self, name, title, abstract, bbox, timesteps):
160
161        self.name = name
162        self.title = title
163        self.abstract = abstract
164        self.bbox = bbox
165        # but no self.children
166        self.timesteps = timesteps
167
168    def _parseName(self):
169        mo = re.match('(.+)\:(.+)\:(.+)', self.name)
170        if mo:
171            (modelName, scenarioName, rest) = mo.groups()
172        else:
173            (modelName, scenarioName, rest) = (None, None, self.name)
174
175        mo2 = re.match('(clim|change)\_(\d+)\/(.+)', rest)
176        if mo2:
177            (type, periodText, description) = mo2.groups()
178            period = int(periodText)
179        else:
180            raise ValueError('Cannot parse in layer name')
181
182        return (type, period, description, modelName, scenarioName)
183
184    def getType(self):
185        return self._parseName()[0]
186
187    def getPeriod(self):
188        '''@return The period length (integer)'''
189        return self._parseName()[1]
190   
191    def getDescription(self):
192        return self._parseName()[2]
193
194    def getModelName(self):
195        return self._parseName()[3]
196
197    def getScenarioName(self):
198        return self._parseName()[4]
199
200    def toKML(self, wmsRequestConfigElement, viewTypes, parentDir, parentDirUrl):
201        '''
202        Overrides the behaviour of C{WMSLayer.toKML}. Returns a C{KML.KMLFolder} object which represents
203        various view of the bottom layer, as per the C{viewTypes} parameter.
204        @param wmsRequestConfigElement: The <WMSRequest> element from the config file (containing information
205        about how WMS requests are to be made, e.g. which server to use, what resolution to use, etc.)
206        @type wmsRequestConfigElement: C{cElementTree.Element}
207        @param viewTypes: Determine in what ways the visible layer data is to be viewed.
208        @type viewTypes: A list of C{View} classes (note: classes, not instances!)
209        @param parentDir: (ignored)
210        layer will be contained directly in this directory.
211        @type parentDir: C{str}
212        @param parentDirUrl: (ignored)
213        @type parentDirUrl: C{str}
214        @return: A C{KML.KMLFolder} object containing individual views of the layers (as sub-folders).
215        '''
216       
217        # For each viewType, generate a new View object, that uses this layer (self) as a model and uses
218        # WMS request configuration wmsRequestConfigElement. The use that object to generate KML for this layer.
219        kmlLayerFolder = KMLFolder(self.title, [], visible = False, opened = False)
220        for viewType in viewTypes:
221            view = viewType(self, wmsRequestConfigElement)
222            kmlLayerFolder.children.append( view.toKML() )
223        return kmlLayerFolder
224
225class View:
226    '''
227    A view of a C{BottomWMSLayer} (as in "model-view-controller", an instance of this class is a view,
228    which the layer is the model). It determines how BottomWMSLayer data can be visualised in KML.
229    In particular, it defines logical transforms of time-points into time-spans.
230    @ivar layer: The layer being viewed.
231    @ivar wmsRequestConfigElement: A config element that defines format of WMS requests.
232    '''
233
234    def __init__(self, layer, wmsRequestConfigElement):
235        '''
236        Initialize the view.
237        @param layer: Some views (not all) may need to "see" the layer data (although some ignore it).
238        @type layer: C{BottomWMSLayer}
239        '''
240        self.layer = layer
241        self.wmsRequestConfigElement = wmsRequestConfigElement
242        self.description = None
243
244    def areCategoriesListedExplicitly(self):
245        '''
246        @returns: A boolean value that signifies whether the self.toKML() method should list the categories
247                  explicitly (in separate KMLFolder's). Must be implemented by all derived classes.
248        '''
249        raise NotImplementedError()
250
251    def getLogicalTimespan(self, timestep):
252        '''
253        Abstract method, defined in derived classes.
254        Translates a single time step into a time span.
255        @param timestep: The date step (a datetime object)
256        @return: The (timespanStart, timespanEnd) tuple (both are datetime objects)
257        '''
258        pass
259
260    def getCategory(self, timestep):
261        '''
262        Abstract method, implemented in derived classes. Get a category in which the timestep belongs to.
263        @param timestep: A timestep.
264        @type timestep: C{pylab.dates.datetime.datetime}
265        @return: The category in which C{timestep} belongs to
266        '''
267        pass
268
269    def getCategoryDescription(self, category):
270        '''
271        Abstract method, defined in derived classes. Get a human-readable description of the category.
272        @param category: The category
273        @type categoty: Undefined (depends on category)
274        @return: A string describing the category (used for naming the category KML folder).
275        '''
276        pass
277
278    def _getSameDate(self, timestep):
279        '''
280        A time-step transform.
281        @type timestep: C{pylab.dates.datetime.datetime}
282        '''
283        return timestep
284
285    def _getFirstDayOfMonth(self, timestep):
286        '''
287        A time-step transform.
288        @type timestep: C{pylab.dates.datetime.datetime}
289        '''
290        return timestep.replace(day=1)
291
292    def _getMonthHence(self, timestep):
293        '''
294        A time-step transform.
295        @type timestep: C{pylab.dates.datetime.datetime}
296        '''
297        if timestep.month+1 <= 12:
298            return timestep.replace(month=timestep.month+1)
299        else:
300            return timestep.replace(year=timestep.year+1, month=1)
301   
302    def _getYearHence(self, timestep):
303        '''
304        A time-step transform.
305        @type timestep: C{pylab.dates.datetime.datetime}
306        '''
307        return timestep.replace(year=timestep.year+1)
308
309    def _getHalfPeriodEarlier(self, timestep):
310        '''
311        A time-step transform.
312        @type timestep: C{pylab.dates.datetime.datetime}
313        @return: A timestep that is half of the viewed layer's period before C{timestep}.
314        '''
315        return timestep.replace(year = timestep.year-self.layer.getPeriod()/2)
316
317    def _getHalfPeriodLater(self, timestep):
318        '''
319        A time-step transform.
320        @type timestep: C{pylab.dates.datetime.datetime}
321        @return: A timestep that is half of the viewed layer's period after C{timestep}.
322        '''
323        return timestep.replace(year = timestep.year+self.layer.getPeriod()/2)
324
325    def toKML(self):
326        '''
327        '''
328
329        def buildWMSRequest(timestep):
330            ''' Build a WMS request '''
331
332            # We will be using configuration for WMS request
333            c = self.wmsRequestConfigElement
334
335            # Set request configuration parameters
336            url = c.find('URL').text
337            serviceVersion = c.find('ServiceVersion').text
338            imageFormat = c.find('ImageFormat').text
339            imageWidth = c.find('ImageWidth').text
340            imageHeight = c.find('ImageHeight').text
341            crs = c.find('CRS').text
342
343            # If the timezone is UTC (which in ISO form would look like 'yyyy-mm-ddThh:mm:ss+00:00'),
344            # then replace it with 'Z'.
345            timestepString = timestep.isoformat()
346            timestepString = timestepString.replace('+00:00', 'Z')
347
348            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)
349
350            return wmsRequest
351
352        def buildKmlGroundOverlay(timestep):
353            (timespanStart, timespanEnd) = self.getLogicalTimespan(timestep)
354            return KMLGroundOverlay(
355                timestep.isoformat(),
356                buildWMSRequest(timestep),
357                timespanStart, timespanEnd,
358                self.layer.bbox.west, self.layer.bbox.south, self.layer.bbox.east, self.layer.bbox.north,
359                visible = False
360                )
361
362        # Create a KML folder that represents the view of the layer
363        kmlLayerViewFolder = KMLFolder(self.name, [], visible = False, opened = False, description = self.description)
364
365        # Create a categorisation dictionary, dict, which will contain categories (as returned by
366        # self.getCategory()) as keys, and timesteps belonging into those categories as values.
367        dict = {}
368        for timestep in self.layer.timesteps:
369            category = self.getCategory(timestep)
370            if not dict.has_key(category):
371                dict[category] = []
372            dict[category].append(timestep)
373        categories = dict.keys()
374        categories.sort()
375
376        for category in categories:
377            categoryDescription = self.getCategoryDescription(category)
378            categoryTimesteps = dict[category]
379            kmlCategoryFolder = KMLFolder(categoryDescription, [], visible = False, opened = False)
380            for timestep in categoryTimesteps:
381                kmlGroundOverlay = buildKmlGroundOverlay(timestep)
382                if self.areCategoriesListedExplicitly():
383                    kmlCategoryFolder.children.append(kmlGroundOverlay)
384                else:
385                    kmlLayerViewFolder.children.append(kmlGroundOverlay)
386            if self.areCategoriesListedExplicitly():
387                kmlLayerViewFolder.children.append(kmlCategoryFolder)
388
389        return kmlLayerViewFolder
390
391class ViewWholeTimecourse(View):
392
393    def __init__(self, layer, wmsRequestConfigElement):
394        View.__init__(self, layer, wmsRequestConfigElement)
395        self.name = 'Whole timecourse'
396        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.'
397        yearSet = set()
398        for timestep in self.layer.timesteps:
399            yearSet.add(timestep.year)
400        self.sortedYears = list(yearSet); self.sortedYears.sort()
401
402    def areCategoriesListedExplicitly(self):
403        return False
404
405    def getLogicalTimespan(self, timestep):
406        category = self.getCategory(timestep)
407        timespanStart = self._getFirstDayOfMonth( timestep.replace(year = category) )
408        timespanEnd = self._getMonthHence(timespanStart)
409        return (timespanStart, timespanEnd)
410
411    def getCategory(self, timestep):
412        try:
413            return self.sortedYears.index(timestep.year) + 1
414        except ValueError:
415            raise ValueError("Timestep's year is not among years that define the categories.")
416
417    def getCategoryDescription(self, category):
418        '''Get a human-readable description of the category (here, return category verbatim).'''
419        return str(category)
420
421class ViewSplittedByMonth(View):
422
423    def __init__(self, layer, wmsRequestConfigElement):
424        View.__init__(self, layer, wmsRequestConfigElement)
425        self.name = 'Compare months'
426        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.'
427
428    def areCategoriesListedExplicitly(self):
429        return True
430
431    def getLogicalTimespan(self, timestep):
432        timespanStart = self._getHalfPeriodEarlier(timestep)
433        timespanEnd = self._getHalfPeriodLater(timestep)
434        return (timespanStart, timespanEnd)
435
436    def getCategory(self, timestep):
437        return timestep.month
438
439    def getCategoryDescription(self, category):
440        '''
441        Get a human-readable description of the category.
442        For instance, for category being 2, the result is 'February'.
443        '''
444        if not ( isinstance(category, int) and category >= 1 and category <= 12 ):
445            raise ValueError('Category not an integer between 1 and 12.')
446        month = category
447        monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 
448                      'July', 'August', 'September', 'October', 'November', 'December']
449        return monthNames[month-1]
450
451class ViewSplittedByPeriod(View):
452
453    def __init__(self, layer, wmsRequestConfigElement):
454        View.__init__(self, layer, wmsRequestConfigElement)
455        self.name = 'Split by period'
456        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.'
457
458    def areCategoriesListedExplicitly(self):
459        return True
460
461    def getLogicalTimespan(self, timestep):
462        timespanStart = self._getFirstDayOfMonth(timestep)
463        timespanEnd = self._getMonthHence(timespanStart)
464        return (timespanStart, timespanEnd)
465
466    def getCategory(self, timestep):
467        return timestep.year
468
469    def getCategoryDescription(self, category):
470        '''
471        Get a human-readable description of the category that timestep belongs to.
472        For instance, for 1990, the result would be 'Period of 1990'.
473        '''
474        if not isinstance(category, int):
475            raise ValueError('Category not an integer (a year)')
476        year = category
477        return 'Period of ' + str(year)
478
479class WMSCapabilities:
480
481    '''[DOC]'''
482
483    def __init__(self):
484        self.topWmsLayer = None
485
486    def parseXML(self, wmsCapabilitiesElement):
487        topLayerElement = wmsCapabilitiesElement.find('{%s}Capability/{%s}Layer' % (wmsXmlNamespace, wmsXmlNamespace))
488        self.topWmsLayer = wmsLayerFactory(topLayerElement)
489
490    def __repr__(self):
491        if self.topWmsLayer:
492            return '--- WMSCapabilities object with top layer as follows): ' + repr(self.topWmsLayer) + ' ---'
493        else:
494            return '--- WMSCapabilities object with no top layer ---'
Note: See TracBrowser for help on using the repository browser.