Coverage for gws-app/gws/plugin/ows_client/wms/caps.py: 100%

50 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""WMS capabilities parser.""" 

2 

3from typing import Optional 

4 

5import gws 

6import gws.base.ows.client 

7import gws.lib.crs 

8import gws.gis.source 

9import gws.lib.xmlx as xmlx 

10import gws.base.ows.client.parseutil as u 

11 

12 

13 

14def parse(xml: str, bottom_first: bool=False) -> gws.OwsCapabilities: 

15 """Read WMS capabilities from the GetCapabilities XML. 

16 

17 Child layers inherit styles, CRSs, the extent, the attribution and the scale 

18 range from their parents, as described in OGC 06-042, 7.2.4.8. Layers without 

19 a name are neither queryable nor rendered as images. 

20 

21 Args: 

22 xml: GetCapabilities XML. 

23 bottom_first: ``True`` if the service lists layers bottom-first; the 

24 layer lists are then reversed to be top-first. 

25 

26 Returns: 

27 The parsed capabilities. 

28 """ 

29 

30 caps_el = xmlx.from_string(xml, gws.XmlOptions(compactWhitespace=True, removeNamespaces=True)) 

31 source_layers = gws.gis.source.check_layers( 

32 [_layer(el) for el in caps_el.findall('Capability/Layer')], 

33 revert=bottom_first 

34 ) 

35 return gws.OwsCapabilities( 

36 metadata=u.service_metadata(caps_el), 

37 operations=u.service_operations(caps_el), 

38 sourceLayers=source_layers, 

39 version=caps_el.get('version')) 

40 

41 

42def _layer(layer_el: gws.XmlElement, parent: Optional[gws.SourceLayer] = None) -> gws.SourceLayer: 

43 """Create a source layer and its children from a ``Layer`` element.""" 

44 sl = gws.SourceLayer() 

45 

46 sl.isQueryable = layer_el.get('queryable') == '1' 

47 sl.isVisible = True 

48 sl.isExpanded = False 

49 sl.metadata = u.element_metadata(layer_el) 

50 sl.name = sl.metadata.get('name', '') 

51 sl.styles = [u.parse_style(e) for e in layer_el.findall('Style')] 

52 sl.title = sl.metadata.get('title', '') 

53 

54 # @TODO: support ScaleHint (WMS 1.1) 

55 

56 smin = layer_el.textof('MinScaleDenominator') 

57 smax = layer_el.textof('MaxScaleDenominator') 

58 if smax: 

59 sl.scaleRange = [u.to_int(smin), u.to_int(smax)] 

60 

61 wgs_extent = u.wgs_extent(layer_el) 

62 crs_list = u.supported_crs(layer_el) 

63 

64 if not parent: 

65 sl.supportedCrs = crs_list or [gws.lib.crs.WGS84] 

66 sl.wgsExtent = wgs_extent 

67 

68 else: 

69 # OGC 06-042, 7.2.4.8 Inheritance of layer properties 

70 

71 # Style -> add 

72 m = {s.name: s for s in sl.styles} 

73 for s in parent.styles: 

74 m[s.name] = s 

75 sl.styles = list(m.values()) 

76 

77 # CRS -> add 

78 sl.supportedCrs = list(parent.supportedCrs) 

79 for crs in crs_list: 

80 if crs not in sl.supportedCrs: 

81 sl.supportedCrs.append(crs) 

82 

83 # EX_GeographicBoundingBox -> replace 

84 sl.wgsExtent = wgs_extent or parent.wgsExtent 

85 

86 # Dimension -> replace 

87 # @TODO 

88 

89 # Attribution -> replace 

90 sl.metadata.attribution = sl.metadata.attribution or parent.metadata.attribution 

91 

92 # AuthorityURL -> add 

93 # @TODO 

94 

95 # MinScaleDenominator -> replace 

96 sl.scaleRange = sl.scaleRange or parent.scaleRange 

97 

98 sl.defaultStyle = u.default_style(sl.styles) 

99 if sl.defaultStyle: 

100 sl.legendUrl = sl.defaultStyle.legendUrl 

101 

102 sl.layers = [_layer(e, sl) for e in layer_el.findall('Layer')] 

103 sl.isGroup = len(sl.layers) > 0 

104 sl.isImage = len(sl.layers) == 0 

105 

106 if not sl.name: 

107 # some folks have unnamed layers in their caps 

108 # we can't render or query them 

109 sl.isQueryable = False 

110 sl.isImage = False 

111 

112 return sl