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

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

In progress changes to make csml2kml compatible with the HiGEM WMS (i.e. WMSs
based on the latest pylons stack). See the new config file for the WMS to
test on.

There are a couple of work arounds marked as "#!TODO" that need cleaning up.

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