2
@brief Parsers for WMS/WMTS/NASA OnEarth capabilities files.
5
- wms_cap_parsers::BaseCapabilitiesTree
6
- wms_cap_parsers::WMSXMLNsHandler
7
- wms_cap_parsers::WMSCapabilitiesTree
8
- wms_cap_parsers::WMTSXMLNsHandler
9
- wms_cap_parsers::WMTSCapabilitiesTree
10
- wms_cap_parsers::OnEarthCapabilitiesTree
12
(C) 2012 by the GRASS Development Team
14
This program is free software under the GNU General Public License
15
(>=v2). Read the file COPYING that comes with GRASS for details.
17
@author Stepan Turek <stepan.turek seznam.cz> (Mentor: Martin Landa)
20
from xml.etree.ElementTree import ParseError
21
except ImportError: # < Python 2.7
22
from xml.parsers.expat import ExpatError as ParseError
24
import xml.etree.ElementTree as etree
25
import grass.script as grass
27
class BaseCapabilitiesTree(etree.ElementTree):
28
def __init__(self, cap_file):
29
"""!Initialize xml.etree.ElementTree
32
etree.ElementTree.__init__(self, file = cap_file)
34
raise ParseError(_("Unable to parse XML file"))
35
except IOError as error:
36
raise ParseError(_("Unable to open XML file '%s'.\n%s\n" % (cap_file, error)))
38
if self.getroot() is None:
39
raise ParseError(_("Root node was not found."))
41
class WMSXMLNsHandler:
42
def __init__(self, caps):
43
"""!Handle XML namespaces according to WMS version of capabilities.
45
self.namespace = "{http://www.opengis.net/wms}"
47
if caps.getroot().find("Service") is not None:
49
elif caps.getroot().find(self.namespace + "Service") is not None:
52
raise ParseError(_("Unable to parse capabilities file.\n\
53
Tag <%s> was not found.") % "Service")
55
def Ns(self, tag_name):
56
"""!Add namespace to tag_name according to version
59
tag_name = self.namespace + tag_name
62
class WMSCapabilitiesTree(BaseCapabilitiesTree):
63
def __init__(self, cap_file, force_version = None):
64
"""!Parses WMS capabilities file.
65
If the capabilities file cannot be parsed if it raises xml.etree.ElementTree.ParseError.
67
The class manges inheritance in 'Layer' elements. Inherited elements
68
are added to 'Layer' element.
69
The class also removes elements which are in invalid form and are needed
70
by wxGUI capabilities dialog.
72
@param cap_file - capabilities file
73
@param force_version - force capabilities file version (1.1.1, 1.3.0)
75
BaseCapabilitiesTree.__init__(self, cap_file)
76
self.xml_ns = WMSXMLNsHandler(self)
78
grass.debug('Checking WMS capabilities tree.', 4)
80
if not "version" in self.getroot().attrib:
81
raise ParseError(_("Missing version attribute root node "
82
"in Capabilities XML file"))
84
wms_version = self.getroot().attrib["version"]
86
if wms_version == "1.3.0":
91
if force_version is not None:
92
if wms_version != force_version:
93
raise ParseError(_("WMS server does not support '%s' version.") % wms_version)
95
capability = self._find(self.getroot(), "Capability")
96
root_layer = self._find(capability, "Layer")
98
self._checkFormats(capability)
99
self._checkLayerTree(root_layer)
101
grass.debug('Check of WMS capabilities tree was finished.', 4)
103
def _checkFormats(self, capability):
104
"""!Check if format element is defined.
106
request = self._find(capability, "Request")
107
get_map = self._find(request, "GetMap")
108
formats = self._findall(get_map, "Format")
110
def _checkLayerTree(self, parent_layer, first = True):
111
"""!Recursively check layer tree and manage inheritance in the tree
114
self._initLayer(parent_layer, None)
116
layers = parent_layer.findall((self.xml_ns.Ns("Layer")))
119
self._initLayer(l, parent_layer)
120
self._checkLayerTree(l, False)
122
def _initLayer(self, layer, parent_layer):
123
"""Inherit elements from parent layer
125
@param layer - <Layer> element which inherits
126
@param parent_layer - <Layer> element which is inherited from
128
if parent_layer is not None:
129
replaced_elements = [ ["EX_GeographicBoundingBox", "replace"],
130
["Attribution", "replace"],
131
["MinScaleDenominator", "replace"],
132
["MaxScaleDenominator", "replace"],
133
["AuthorityURL", "add"]]
135
for element in replaced_elements:
136
elems = layer.findall(self.xml_ns.Ns(element[0]))
138
if len(elems) != 0 or element[1] == "add":
139
for e in parent_layer.findall(self.xml_ns.Ns(element[0])):
142
inh_arguments = ["queryable", "cascaded", "opaque",
143
"noSubsets", "fixedWidth", "fixedHeight"]
145
for attr in parent_layer.attrib:
146
if attr not in layer.attrib and attr in inh_arguments:
147
layer.attrib[attr] = parent_layer.attrib[attr]
149
self._inhNotSame(self.proj_tag, "element_content", layer, parent_layer)
150
self._inhNotSame("BoundingBox", "attribute", layer, parent_layer, self.proj_tag)
152
# remove invalid Styles
153
styles = layer.findall(self.xml_ns.Ns('Style'))
155
s_name = s.find(self.xml_ns.Ns('Name'))
156
if s_name is None or not s_name.text:
157
grass.debug('Removed invalid <Style> element.', 4)
160
self._inhNotSame("Style", "child_element_content", layer, parent_layer, "Name")
161
self._inhNotSame("Dimension", "attribute", layer, parent_layer, "name")
163
def _inhNotSame(self, element_name, cmp_type, layer, parent_layer, add_arg = None):
164
"""Inherit elements which have unique values.
166
@param element_name - name of inherited element
167
@param cmp_type - 'element_content' - compared value is text of <Layer> element
168
@param cmp_type - 'child_element_content' - compared value is text of a child of the <Layer> element
169
@param cmp_type - 'attribute' - compared value is text of the <Layer> element attribute
170
@param layer - <Layer> element which inherits
171
@param parent_layer - <Layer> element which is inherited from
172
@param add_arg - name of child element or attribute
174
elem = layer.findall(self.xml_ns.Ns(element_name))
177
if parent_layer is not None:
178
parent_elems = parent_layer.findall(self.xml_ns.Ns(element_name))
180
for par_elem in parent_elems:
181
parent_cmp_text = None
182
if cmp_type == "attribute":
183
if add_arg in par_elem.attrib:
184
parent_cmp_text = par_elem.attrib[add_arg];
186
elif cmp_type == "element_content":
187
parent_cmp_text = par_elem.text
189
elif cmp_type == "child_element_content":
190
parent_cmp = par_elem.find(self.xml_ns.Ns(add_arg))
191
if parent_cmp is not None:
192
parent_cmp_text = parent_cmp.text
194
if parent_cmp_text is None:
200
if cmp_type == "attribute":
201
if add_arg in elem.attrib:
202
cmp_text = elem.attrib[add_arg]
204
elif cmp_type == "element_content":
207
elif cmp_type == "child_element_content":
208
cmp = elem.find(self.xml_ns.Ns(add_arg))
212
if cmp_text is None or \
213
cmp_text.lower() == parent_cmp_text.lower():
218
layer.append(par_elem)
220
def _find(self, etreeElement, tag):
221
"""!Find child element.
222
If the element is not found it raises xml.etree.ElementTree.ParseError.
224
res = etreeElement.find(self.xml_ns.Ns(tag))
227
raise ParseError(_("Unable to parse capabilities file. \n\
228
Tag <%s> was not found.") % tag)
232
def _findall(self, etreeElement, tag):
233
"""!Find all children element.
234
If no element is found it raises xml.etree.ElementTree.ParseError.
236
res = etreeElement.findall(self.xml_ns.Ns(tag))
239
raise ParseError(_("Unable to parse capabilities file. \n\
240
Tag <%s> was not found.") % tag)
244
def getprojtag(self):
245
"""!Return projection tag according to version of capabilities (CRS/SRS).
249
def getxmlnshandler(self):
250
"""!Return WMSXMLNsHandler object.
254
class WMTSXMLNsHandler:
255
"""!Handle XML namespaces which are used in WMTS capabilities file.
257
def NsWmts(self, tag):
260
return "{http://www.opengis.net/wmts/1.0}" + tag
262
def NsOws(self, tag):
265
return "{http://www.opengis.net/ows/1.1}" + tag
267
class WMTSCapabilitiesTree(BaseCapabilitiesTree):
268
def __init__(self, cap_file):
269
"""!Parses WMTS capabilities file.
270
If the capabilities file cannot be parsed it raises xml.etree.ElementTree.ParseError.
272
The class also removes elements which are in invalid form and are needed
273
by wxGUI capabilities dialog or for creation of GetTile request by GRASS WMS library.
275
@param cap_file - capabilities file
277
BaseCapabilitiesTree.__init__(self, cap_file)
278
self.xml_ns = WMTSXMLNsHandler()
280
grass.debug('Checking WMTS capabilities tree.', 4)
282
contents = self._find(self.getroot(), 'Contents', self.xml_ns.NsWmts)
284
tile_mat_sets = self._findall(contents, 'TileMatrixSet', self.xml_ns.NsWmts)
286
for mat_set in tile_mat_sets:
287
if not self._checkMatSet(mat_set):
288
grass.debug('Removed invalid <TileMatrixSet> element.', 4)
289
contents.remove(mat_set)
291
# are there any <TileMatrixSet> elements after the check
292
self._findall(contents, 'TileMatrixSet', self.xml_ns.NsWmts)
294
layers = self._findall(contents, 'Layer', self.xml_ns.NsWmts)
296
if not self._checkLayer(l):
297
grass.debug('Removed invalid <Layer> element.', 4)
300
# are there any <Layer> elements after the check
301
self._findall(contents, 'Layer', self.xml_ns.NsWmts)
303
grass.debug('Check of WMTS capabilities tree was finished.', 4)
305
def _checkMatSet(self, mat_set):
306
"""!Check <TileMatrixSet>.
308
mat_set_id = mat_set.find(self.xml_ns.NsOws('Identifier'))
309
if mat_set_id is None or not mat_set_id.text:
312
mat_set_srs = mat_set.find(self.xml_ns.NsOws('SupportedCRS'))
313
if mat_set_srs is None or \
314
not mat_set_srs.text:
317
tile_mats = mat_set.findall(self.xml_ns.NsWmts('TileMatrix'))
321
for t_mat in tile_mats:
322
if not self._checkMat(t_mat):
323
grass.debug('Removed invalid <TileMatrix> element.', 4)
324
mat_set.remove(t_mat)
326
tile_mats = mat_set.findall(self.xml_ns.NsWmts('TileMatrix'))
332
def _checkMat(self, t_mat):
333
"""!Check <TileMatrix>.
335
def _checkElement(t_mat, e, func):
336
element = t_mat.find(self.xml_ns.NsWmts(e))
337
if element is None or not element.text:
341
e = func(element.text)
349
for e, func in [['ScaleDenominator', float],
351
['TileHeight', int]]:
352
if not _checkElement(t_mat, e, func):
355
tile_mat_id = t_mat.find(self.xml_ns.NsOws('Identifier'))
356
if tile_mat_id is None or not tile_mat_id.text:
359
tl_str = t_mat.find(self.xml_ns.NsWmts('TopLeftCorner'))
360
if tl_str is None or not tl_str.text:
363
tl = tl_str.text.split(' ')
374
def _checkLayer(self, layer):
375
"""!Check <Layer> element.
377
layer_id = layer.find(self.xml_ns.NsOws('Identifier'))
378
if layer_id is None or not layer_id.text:
381
mat_set_links = layer.findall(self.xml_ns.NsWmts('TileMatrixSetLink'))
382
if not mat_set_links:
385
styles = layer.findall(self.xml_ns.NsWmts('Style'))
390
s_name = s.find(self.xml_ns.NsOws('Identifier'))
391
if s_name is None or not s_name.text:
392
grass.debug('Removed invalid <Style> element.', 4)
395
contents = self.getroot().find(self.xml_ns.NsWmts('Contents'))
396
mat_sets = contents.findall(self.xml_ns.NsWmts('TileMatrixSet'))
398
for link in mat_set_links:
399
# <TileMatrixSetLink> does not point to existing <TileMatrixSet>
400
if not self._checkMatSetLink(link, mat_sets):
401
grass.debug('Removed invalid <TileMatrixSetLink> element.', 4)
406
def _checkMatSetLink(self, link, mat_sets):
407
"""!Check <TileMatrixSetLink> element.
409
mat_set_link_id = link.find(self.xml_ns.NsWmts('TileMatrixSet')).text
412
for mat_set in mat_sets:
413
mat_set_id = mat_set.find(self.xml_ns.NsOws('Identifier')).text
415
if mat_set_id != mat_set_link_id:
418
# the link points to existing <TileMatrixSet>
421
tile_mat_set_limits = link.find(self.xml_ns.NsWmts('TileMatrixSetLimits'))
422
if tile_mat_set_limits is None:
425
tile_mat_limits = tile_mat_set_limits.findall(self.xml_ns.NsWmts('TileMatrixLimits'))
426
for limit in tile_mat_limits:
427
if not self._checkMatSetLimit(limit):
428
grass.debug('Removed invalid <TileMatrixLimits> element.', 4)
429
tile_mat_limits.remove(limit)
431
# are there any <TileMatrixLimits> elements after the check
432
tile_mat_limits = tile_mat_set_limits.findall(self.xml_ns.NsWmts('TileMatrixLimits'))
433
if not tile_mat_limits:
434
grass.debug('Removed invalid <TileMatrixSetLimits> element.', 4)
435
link.remove(tile_mat_set_limits)
442
def _checkMatSetLimit(self, limit):
443
"""!Check <TileMatrixLimits> element.
445
limit_tile_mat = limit.find(self.xml_ns.NsWmts('TileMatrix'))
446
if limit_tile_mat is None or not limit_tile_mat.text:
449
for i in ['MinTileRow', 'MaxTileRow', 'MinTileCol', 'MaxTileCol']:
450
i_tag = limit.find(self.xml_ns.NsWmts(i))
459
def _find(self, etreeElement, tag, ns = None):
460
"""!Find child element.
461
If the element is not found it raises xml.etree.ElementTree.ParseError.
464
res = etreeElement.find(tag)
466
res = etreeElement.find(ns(tag))
469
raise ParseError(_("Unable to parse capabilities file. \n\
470
Tag '%s' was not found.") % tag)
474
def _findall(self, etreeElement, tag, ns = None):
475
"""!Find all children element.
476
If no element is found it raises xml.etree.ElementTree.ParseError.
479
res = etreeElement.findall(tag)
481
res = etreeElement.findall(ns(tag))
484
raise ParseError(_("Unable to parse capabilities file. \n\
485
Tag '%s' was not found.") % tag)
489
def getxmlnshandler(self):
490
"""!Return WMTSXMLNsHandler object.
494
class OnEarthCapabilitiesTree(BaseCapabilitiesTree):
495
def __init__(self, cap_file):
496
"""!Parse NASA OnEarth tile service file.
497
If the file cannot be parsed it raises xml.etree.ElementTree.ParseError.
499
The class also removes elements which are in invalid form and are needed
500
by wxGUI capabilities dialog or for creation of GetMap request by GRASS WMS library.
502
@param cap_file - capabilities file
504
BaseCapabilitiesTree.__init__(self, cap_file)
506
grass.debug('Checking OnEarth capabilities tree.', 4)
508
self._checkLayerTree(self.getroot())
510
grass.debug('Check if OnEarth capabilities tree was finished.', 4)
512
def _checkLayerTree(self, parent_layer, first = True):
513
"""!Recursively check layer tree.
516
tiled_patterns = self._find(parent_layer, 'TiledPatterns')
517
layers = tiled_patterns.findall('TiledGroup')
518
layers += tiled_patterns.findall('TiledGroups')
519
parent_layer = tiled_patterns
521
layers = parent_layer.findall('TiledGroup')
522
layers += parent_layer.findall('TiledGroups')
525
if not self._checkLayer(l):
526
grass.debug(('Removed invalid <%s> element.' % l.tag), 4)
527
parent_layer.remove(l)
528
if l.tag == 'TiledGroups':
529
self._checkLayerTree(l, False)
531
def _find(self, etreeElement, tag):
532
"""!Find child element.
533
If the element is not found it raises xml.etree.ElementTree.ParseError.
535
res = etreeElement.find(tag)
538
raise ParseError(_("Unable to parse tile service file. \n\
539
Tag <%s> was not found.") % tag)
543
def _checkLayer(self, layer):
544
"""!Check <TiledGroup>/<TiledGroups> elements.
546
if layer.tag == 'TiledGroups':
549
name = layer.find('Name')
550
if name is None or not name.text:
553
t_patts = layer.findall('TilePattern')
556
urls = self._getUrls(patt)
558
if not self.gettilepatternurldata(url):
561
# check if there are any vaild urls
563
grass.debug('<TilePattern> was removed. It has no valid url.', 4)
565
patt.text = '\n'.join(urls)
567
t_patts = layer.findall('TilePattern')
573
def _getUrls(self, tile_pattern):
574
"""!Get all urls from tile pattern.
577
if tile_pattern.text is not None:
578
tile_patt_lines = tile_pattern.text.split('\n')
580
for line in tile_patt_lines:
581
if 'request=GetMap' in line:
582
urls.append(line.strip())
585
def gettilepatternurldata(self, url):
586
"""!Parse url string in Tile Pattern.
588
par_url = bbox = width = height = None
590
bbox_idxs = self.geturlparamidxs(url, "bbox=")
591
if bbox_idxs is None:
594
par_url = [url[:bbox_idxs[0] - 1], url[bbox_idxs[1]:]]
596
bbox = url[bbox_idxs[0] + len('bbox=') : bbox_idxs[1]]
597
bbox_list = bbox.split(',')
598
if len(bbox_list) < 4:
602
bbox = map(float, bbox.split(','))
606
width_idxs = self.geturlparamidxs(url, "width=")
607
if width_idxs is None:
611
width = int(url[width_idxs[0] + len('width=') : width_idxs[1]])
615
height_idxs = self.geturlparamidxs(url, "height=")
616
if height_idxs is None:
620
height = int(url[height_idxs[0] + len('height=') : height_idxs[1]])
624
if height < 0 or width < 0:
627
return par_url, bbox, width, height
629
def geturlparamidxs(self, params_str, param_key):
630
"""!Find start and end index of parameter and it's value in url string
632
start_i = params_str.lower().find(param_key)
635
end_i = params_str.find("&", start_i)
637
end_i = len(params_str)
639
return (start_i, end_i)