/[thuban]/trunk/thuban/Extensions/wms/parser.py
ViewVC logotype

Annotation of /trunk/thuban/Extensions/wms/parser.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2173 - (hide annotations)
Tue Apr 13 19:28:05 2004 UTC (20 years, 10 months ago) by joey
File MIME type: text/x-python
File size: 17804 byte(s)
Added support for oldstyle (WMS 1.0 apparently) image format specification.

1 joey 2133 # Copyright (c) 2004 by Intevation GmbH
2     # Authors:
3     # Martin Schulze <[email protected]>
4     #
5     # This program is free software; you can redistribute it and/or modify
6     # it under the terms of the GNU General Public License as published by
7     # the Free Software Foundation; either version 2 of the License, or
8     # (at your option) any later version.
9     #
10     # This program is distributed in the hope that it will be useful,
11     # but WITHOUT ANY WARRANTY; without even the implied warranty of
12     # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13     # GNU General Public License for more details.
14     #
15     # You should have received a copy of the GNU General Public License
16     # along with this program; if not, write to the Free Software
17     # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18    
19     """
20     Inspect WMS Capabilities for later processing.
21    
22     Information should only be retrieved with the proper get*() methods.
23    
24     class WMSCapabilitiesParser:
25     __init__()
26    
27     grok(string)
28    
29     getTitle()
30     getAbstract()
31     getFees()
32     getAccessConstraints()
33     getFormats()
34     getLayers()
35     getSRS()
36    
37     getLayerTitle(layer)
38     getLayerSRS(layer)
39     getLayerLatLonBBox(layer)
40     getLayerBBox(layer, srs)
41    
42     isQueryable(layer)
43    
44     get_srs_discrepancies()
45     """
46    
47     __version__ = "$Revision$"
48     # $Source$
49     # $Id$
50    
51     import xml.dom.minidom
52 joey 2173 from xml.dom import Node
53 joey 2133
54     from domutils import getElementsByName, getElementByName
55    
56 joey 2165 from Thuban import _
57    
58 joey 2133 class WMSCapabilitiesParser:
59     """
60     Thuban class to parse capabilities supplied as large string.
61    
62     This class provides methods to parse and retrieve particular
63     information from the WMS capabilities XML. Information should
64     only be extracted by the respective get*() methods.
65     """
66    
67     layers = None
68     title = None
69     abstract = None
70     fees = None
71     access = None
72     formats = None
73     srs_discrepancies = None
74 joey 2165 error = None
75 joey 2133
76    
77     def __init__(self):
78     """
79     Initialises an instance in this class.
80     """
81    
82     # Note that we must not initialise internal variables of the
83     # class in a mutable way or it will be shared among all
84     # instances. None is immutable, [] is not.
85 joey 2165 self.error = []
86 joey 2133
87    
88     def grok(self, data):
89     """
90     Parses the XML response to a WMS GetCapabilities request.
91    
92     Internal datastructure of the class will be filled.
93     Information should only be retrieved with the respective
94     get*() methods.
95     """
96 joey 2153 xml_dom = xml.dom.minidom.parseString(data)
97     root = xml_dom.documentElement
98 joey 2133
99     # Extract the title
100     foo = getElementByName(getElementByName(root, 'Service'), 'Title')
101     if foo:
102     self.title = foo.childNodes[0].data
103    
104     # Extract the abstract
105     foo = getElementByName(getElementByName(root, 'Service'), 'Abstract')
106     if foo and len(foo.childNodes[0].data):
107     self.abstract = foo.childNodes[0].data
108    
109     # Extract fees information
110     foo = getElementByName(getElementByName(root, 'Service'), 'Fees')
111     if foo and len(foo.childNodes[0].data) \
112 joey 2155 and foo.childNodes[0].data.lower() != 'none':
113 joey 2133 self.fees = foo.childNodes[0].data
114    
115     # Extract access information
116     foo = getElementByName(getElementByName(root, 'Service'),
117     'AccessConstraints')
118     if foo and len(foo.childNodes[0].data) \
119 joey 2155 and foo.childNodes[0].data.lower() != 'none':
120 joey 2133 self.access = foo.childNodes[0].data
121    
122 joey 2173 foo = getElementByName(getElementByName(
123     root, 'Capability'), 'Request')
124 joey 2133
125 joey 2173 # Need to distinguish between Map and GetMap for v1.0 and v1.1
126     bar = getElementByName(foo, 'GetMap')
127     if bar:
128     # WMS 1.1
129     foo = getElementsByName(bar, 'Format')
130     self.formats = map((lambda i: i.childNodes[0].data), foo)
131     else:
132     # WMS 1.0
133     foo = getElementByName(getElementByName(
134     foo, 'Map'), 'Format')
135     for node in foo.childNodes:
136     if node.nodeType == Node.ELEMENT_NODE:
137     try:
138     self.formats.append(node.nodeName)
139     except AttributeError:
140     self.formats = [node.nodeName]
141    
142 joey 2133 # Extract layer names
143     self.layers = []
144     self.peekLayers(getElementByName(getElementByName(
145     root, 'Capability'), 'Layer'), -1)
146    
147 joey 2153 xml_dom.unlink()
148 joey 2133
149 joey 2153
150 joey 2133 def peekLayers(self, top, parent):
151     """
152     Inspect the provided DOM fragment referenced as top.
153    
154     This method will inspect all included layers and traverse the
155     tree recursively in order to fill the internal datastructure.
156    
157     Note that SRS other than EPSG:* are not yet supported,
158     especially there is no support for AUTO and NONE.
159     """
160    
161     index = len (self.layers)
162     self.layers.append({})
163     self.layers[index]['parent'] = parent
164    
165     for foo in top.attributes.keys():
166     if foo == 'queryable':
167     self.layers[index]['queryable'] \
168     = int(top.attributes.get(foo).nodeValue)
169    
170     foo = getElementByName(top, 'Title')
171     if foo and len(foo.childNodes[0].data):
172     self.layers[index]['title'] = foo.childNodes[0].data
173 joey 2165 else:
174     # A <Title> is required for each layer, <name> is optional
175     # See OGC 01-068r3, 7.1.4.5.1 and 7.1.4.5.2
176     self.error.append(_("No title found for layer #%d") % index)
177 joey 2133
178     foo = getElementByName(top, 'Name')
179     if foo and len(foo.childNodes[0].data):
180     self.layers[index]['name'] = foo.childNodes[0].data
181    
182     # These values are only used for an integrity check
183     for foo in getElementsByName(top, 'SRS'):
184     for srs in foo.childNodes[0].data.split(" "):
185     if srs[0:5] == 'EPSG:':
186     srs = srs[5:]
187     try:
188 joey 2167 int(srs)
189     try:
190     self.layers[index]['srs'].append(srs)
191     except KeyError:
192     self.layers[index]['srs'] = [srs]
193     except ValueError:
194     if srs[0:4].upper() == 'AUTO' \
195     or srs[0:4].upper() == 'NONE':
196     try:
197     self.layers[index]['_srs_'].append(srs)
198     except KeyError:
199     self.layers[index]['_srs_'] = [srs]
200     else:
201     self.error.append(_("SRS '%s' is not numerical and not"
202     " AUTO/NONE in layer '%s'") \
203     % (srs, self.layers[index]['title']))
204 joey 2133
205     foo = getElementByName(top, 'LatLonBoundingBox')
206     if foo is not None:
207     self.layers[index]['llbbox'] = {}
208     for corner in ['minx', 'miny', 'maxx', 'maxy']:
209     self.layers[index]['llbbox'][corner] \
210     = foo.attributes.get(corner).nodeValue
211    
212     boxes = getElementsByName(top, 'BoundingBox')
213     if boxes != []:
214     self.layers[index]['bbox'] = {}
215     for foo in boxes:
216     srs = foo.attributes.get('SRS').nodeValue
217     if srs[0:5] == 'EPSG:':
218     srs = srs[5:]
219     self.layers[index]['bbox'][srs] = {}
220     for corner in ['minx', 'miny', 'maxx', 'maxy']:
221     self.layers[index]['bbox'][srs][corner] \
222     = foo.attributes.get(corner).nodeValue
223    
224     # Traverse subsidiary layers
225     sublayer = getElementsByName(top, 'Layer')
226     for l in sublayer:
227     self.peekLayers(l, index)
228    
229    
230     def getTitle(self):
231     """
232     Returns the main title of the WMS object.
233    
234     If no title is provided in the capabilities, an empty string
235     is returned.
236     """
237     if self.title is None:
238     return ''
239     else:
240     return self.title
241    
242    
243     def getAbstract(self):
244     """
245     Returns the abstract of the WMS object.
246    
247     If no abstract is provided in the capabilities, an empty
248     string is returned.
249     """
250     if self.abstract is None:
251     return ''
252     else:
253     return self.abstract
254    
255    
256     def getFees(self):
257     """
258     Returns the fees information of the WMS object.
259    
260     If no information is provided in the capabilities or if it is
261     set to 'none', an empty string is returned.
262     """
263     if self.fees is None:
264     return ''
265     else:
266     return self.fees
267    
268    
269     def getAccessConstraints(self):
270     """
271     Returns information about access constraints for the WMS object.
272    
273     If no information is provided in the capabilities or if it is
274     set to 'none', an empty string is returned.
275     """
276     if self.access is None:
277     return ''
278     else:
279     return self.access
280    
281    
282     def getFormats(self):
283     """
284     Returns a list of supported output formats.
285    
286     These are used in the GetMap request. This method will
287     default to 'image/jpeg' if no format is recognised in XML
288     Capabilities, assuming that JPEG will always be supported on
289     the server side.
290     """
291     if self.formats is None:
292     return ['image/jpeg']
293     else:
294     return self.formats
295    
296     def getLayers(self):
297     """
298     Returns a list of layer names.
299    
300     Only named layers will be returned, since a layer may have a
301     title but doesn't have to have a name associated to it as
302     well. If no layers were found, an empty list is returned.
303     """
304     result = []
305     for layer in self.layers:
306     if 'name' in layer:
307     result.append(layer['name'])
308    
309     return result
310    
311    
312     def getSRS(self):
313     """
314     Returns the root list of spatial reference systems (SRS).
315    
316     This list is taken from the root layer. Those SRS are common
317     to all subsidiary layers. If no SRS are common to all layers,
318     an empty list is returned. If no layers were detected, an
319     empty list is returned as well.
320     """
321     if len(self.layers) == 0:
322     return []
323    
324     # index 0 denotes the root layer by design
325     if 'srs' in self.layers[0].keys():
326     return self.layers[0]['srs']
327    
328    
329     def getLayerTitle(self, name):
330     """
331     Returns the title of the named layer.
332    
333     If no such title or no such layer exists, an empty string is
334     returned.
335     """
336     for layer in self.layers:
337     if 'name' in layer:
338     if layer['name'] == name:
339     if 'title' in layer:
340     return layer['title']
341    
342     return ''
343    
344    
345     def getLayerSRS(self, name):
346     """
347     Returns a list of spacial reference systems (SRS).
348    
349     The SRS are specified by the European Petroleum Survey Group
350     (EPSG). There should be at least one SRS per layer, though.
351     The prefix 'EPSG:' will be stripped. If none exists, an empty
352     list is returned.
353    
354     The specification [OGC 01-068r3] says about inheritance of
355     SRS:
356    
357     - Every layer is available in one or more SRS (or in an
358     undefined SRS)
359    
360     - Geographic information whose SRS is undefined
361     (e.g. digitised historical maps) shall use 'NONE'
362     (case-sensitive). This implementation does not support
363     this.
364    
365     - Every layer shall have at least one SRS element that is
366     either stated explicitly or inherited from a parent layer.
367    
368     - The root layer element shall include a sequence of zero or
369     more SRS elements listing all SRS which are common for to
370     all subsidiary layers.
371    
372     - Layers may optionally add to the global SRS list, or to the
373     list inherited from a parent layer.
374    
375 joey 2144 - A server which has the ability to transform data to
376     different SRSes may choose not to provide an explicit
377     BoundingBox for every possible SRS available for each Layer.
378     Thus the list of <SRS> elements are authoritative.
379    
380 joey 2133 This implementation returns the list of SRS for the given
381     layer, calculated by looking at BoundingBoxes defined in the
382     named layer and all layers higher in the hierarchy up to the
383     root layer which weren't overwritten.
384     """
385     for i in range(len(self.layers)):
386     if 'name' in self.layers[i]:
387     if self.layers[i]['name'] == name:
388     pivot = i
389     break
390     else:
391     return []
392    
393     result = []
394     while pivot != -1:
395 joey 2144 if 'srs' in self.layers[pivot]:
396     for srs in self.layers[pivot]['srs']:
397 joey 2133 if srs not in result:
398     result.append(srs)
399     pivot = self.layers[pivot]['parent']
400    
401     return result
402    
403    
404     def getLayerLatLonBBox(self, name):
405     """
406     Returns a dictionary of the LatLonBoundingBox.
407    
408     ... for the named layer if it exists. The SRS is always
409     EPSG:4326 per convention. There should be at least one,
410     though, inherited from the root layer at least. If none
411     exists, the value None is returned.
412     """
413     for layer in self.layers:
414     if 'name' in layer:
415     if layer['name'] == name:
416     if 'llbbox' in layer:
417     return layer['llbbox']
418     else:
419     # No LatLonBoundingBox found in current layer,
420     # so traverse the hierarchy upwards and check
421     # again until there is one.
422     pivot = layer
423     while pivot['parent'] != -1:
424     pivot = self.layers[pivot['parent']]
425     if 'llbbox' in pivot:
426     return pivot['llbbox']
427    
428     return None
429    
430    
431     def getLayerBBox(self, name, srs):
432     """
433     Returns a dictionary of the BoundingBox.
434    
435     If no such BoundingBox exists, None is returned.
436    
437     The specification [OGC 01-068r3] says about BoundingBoxes:
438    
439     - Layers may have zero or more BoundingBox elements what are
440     either stated explicitly or inherited from a parent layer.
441    
442     - A layer may have multiple BoundingBox elements, but each one
443     shall state a different SRS.
444    
445     - A layer inherits any BoundingBoxes defined by its
446     parents.
447    
448     - A BoundingBox inherited from the parent layer for a
449     particular SRS is replaced by any declaration for the same
450     SRS in the current layer.
451    
452     - A BoundingBox in the child layer for a new SRS which is not
453     already declared by the parent, is added to the list of
454     BoundingBoxes for the child layer.
455    
456     - A single layer shall not contain more than one BoundingBox
457     element for the same SRS.
458     """
459     for layer in self.layers:
460     if 'name' in layer:
461     if layer['name'] == name:
462     if 'bbox' in layer:
463     if srs in layer['bbox']:
464     return layer['bbox'][srs]
465    
466     # No BoundingBox for the given SRS found in
467     # current layer, so traverse the hierarchy upwards
468     # and check again until there is one.
469     pivot = layer
470     while pivot['parent'] != -1:
471     pivot = self.layers[pivot['parent']]
472     if 'bbox' in pivot:
473     if srs in pivot['bbox']:
474     return pivot['bbox'][srs]
475    
476 joey 2170 # No matching BBox found, let's see if it was EPSG:4326
477     if srs == '4326':
478     return self.getLayerLatLonBBox(name)
479    
480 joey 2133 return None
481    
482    
483     def isQueryable(self, name):
484     """
485     Returns the value of the queryable attribute of a layer.
486    
487     This attribute denotes whether this layer can be queried
488     through the GetFeatureInfo request. The default value is 0.
489    
490     The specification [OGC 01-068r3] this attribute can be
491     inherited.
492     """
493    
494     for layer in self.layers:
495     if 'name' in layer:
496     if layer['name'] == name:
497     try:
498     return layer['queryable']
499     except KeyError:
500     # No attribute in this layer, so traverse the
501     # hierarchy upwards
502     pivot = layer
503     while pivot['parent'] != -1:
504     pivot = self.layers[pivot['parent']]
505     if 'queryable' in pivot:
506     return pivot['queryable']
507     return 0
508    
509    
510     def get_srs_discrepancies(self):
511     """
512     Returns a list of layer identifications where the denoted SRS
513     values differ from the calculated ones through BoundingBox
514     elements.
515     """
516     return self.srs_discrepancies
517    
518    
519     if __name__ == "__main__":
520     print "This module cannot be executed standalone."
521    
522     import os
523    
524 joey 2164 sample = "test/sample.xml"
525 joey 2133 try:
526 joey 2164 f = open(sample, "r")
527 joey 2133 except IOError:
528     try:
529 joey 2164 f = open(os.path.dirname(__file__) + "/" + sample, "r")
530 joey 2133 except IOError:
531 joey 2164 print "Cannot open %s for reading" % sample
532 joey 2133
533     if f is not None:
534     sample = f.read();
535     f.close()
536    
537     ina = WMSCapabilitiesParser()
538     ina.grok(sample)

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26