Coverage for gws-app/gws/plugin/ows_client/wfs/provider.py: 0%

72 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1"""WFS provider. 

2 

3References: 

4 

5- wfs 1.0.0: http://portal.opengeospatial.org/files/?artifact_id=7176 Sec 13.7.3 

6- wfs 1.1.0: http://portal.opengeospatial.org/files/?artifact_id=8339 Sec 14.7.3 

7- wfs 2.0.0: http://docs.opengeospatial.org/is/09-025r2/09-025r2.html Sec 11.1.3 

8 

9See also: 

10 

11- https://docs.geoserver.org/latest/en/user/services/wfs/reference.html 

12 

13""" 

14 

15from typing import Optional, cast 

16 

17import gws 

18import gws.base.ows.client 

19import gws.base.shape 

20import gws.config.util 

21import gws.lib.bounds 

22import gws.lib.crs 

23import gws.lib.extent 

24import gws.gis.source 

25 

26from . import caps 

27 

28 

29class Config(gws.base.ows.client.provider.Config): 

30 """WFS provider configuration.""" 

31 

32 withBboxCrs: Optional[bool] 

33 """Add CRS to bbox request parameters.""" 

34 

35 

36class Object(gws.base.ows.client.provider.Object): 

37 protocol = gws.OwsProtocol.WFS 

38 withBboxCrs: bool 

39 isWfs2: bool 

40 

41 def configure(self): 

42 cc = caps.parse(self.get_capabilities()) 

43 

44 self.metadata = cc.metadata 

45 self.sourceLayers = cc.sourceLayers 

46 self.version = cc.version 

47 self.isWfs2 = self.version >= '2' 

48 

49 self.configure_operations(cc.operations) 

50 

51 # use bbox with crs for wfs 2 by default 

52 # see also comments in qgis/qgswfsfeatureiterator.cpp buildURL 

53 p = self.cfg('withBboxCrs') 

54 self.withBboxCrs = self.isWfs2 if p is None else p 

55 

56 DEFAULT_GET_FEATURE_LIMIT = 100 

57 

58 def get_features(self, search, source_layers): 

59 """Perform the WFS GetFeature operation. 

60 

61 We only do spatial searches here. 

62 If no bounds and no shapes are given, return all features. 

63 If a shape is given, find features within its bounds first, 

64 and filter features on our side. 

65 This is more performant than WFS spatial ops (at least for qgis), 

66 and also works without spatial ops support on the provider side. 

67 """ 

68 

69 bounds = search.bounds 

70 search_shape = None 

71 

72 if search.shape: 

73 geometry_tolerance = 0.0 

74 

75 if search.tolerance: 

76 n, u = search.tolerance 

77 geometry_tolerance = n * (search.resolution or 1) if u == 'px' else n 

78 

79 search_shape = search.shape.tolerance_polygon(geometry_tolerance) 

80 bounds = search_shape.bounds() 

81 

82 if not bounds: 

83 gws.log.warning('get_features: no bounds or shape given') 

84 return [] 

85 

86 request_crs = self.forceCrs or gws.lib.crs.WGS84 

87 

88 bbox = gws.lib.bounds.transform(bounds, request_crs).extent 

89 if request_crs.isYX and not self.alwaysXY: 

90 bbox = gws.lib.extent.swap_xy(bbox) 

91 bbox = ','.join(str(k) for k in bbox) 

92 

93 srs = request_crs.urn 

94 if self.withBboxCrs: 

95 bbox += ',' + srs 

96 

97 params = { 

98 'BBOX': bbox, 

99 'COUNT' if self.isWfs2 else 'MAXFEATURES': search.limit or self.DEFAULT_GET_FEATURE_LIMIT, 

100 'SRSNAME': srs, 

101 'TYPENAMES' if self.isWfs2 else 'TYPENAME': [sl.name for sl in source_layers], 

102 'VERSION': self.version, 

103 } 

104 

105 if search.extraParams: 

106 params = gws.u.merge(params, gws.u.to_upper_dict(search.extraParams)) 

107 

108 op = self.get_operation(gws.OwsVerb.GetFeature) 

109 if not op: 

110 return [] 

111 

112 if op.preferredFormat: 

113 params.setdefault('OUTPUTFORMAT', op.preferredFormat) 

114 

115 args = self.prepare_operation(op, params=params) 

116 text = gws.base.ows.client.request.get_text(args) 

117 

118 try: 

119 records = gws.base.ows.client.featureinfo.parse(text, default_crs=request_crs, always_xy=self.alwaysXY) 

120 except gws.Error as exc: 

121 gws.log.error(f'get_features: parse error: {exc!r}') 

122 return [] 

123 

124 gws.log.debug(f'get_features: FOUND={len(records)} params={params!r}') 

125 

126 for rec in records: 

127 if rec.shape: 

128 rec.shape = rec.shape.transformed_to(bounds.crs) 

129 

130 if not search_shape: 

131 return records 

132 

133 filtered = [ 

134 rec for rec in records 

135 if not rec.shape or rec.shape.intersects(search_shape) 

136 ] 

137 

138 gws.log.debug(f'get_features: FILTERED={len(filtered)}') 

139 return filtered