Coverage for gws-app/gws/plugin/ows_server/wfs/__init__.py: 0%

96 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1"""WFS Service. 

2 

3Implements WFS 2.0 "Basic" profile. 

4This implementation only supports ``GET`` requests with ``KVP`` encoding. 

5 

6Supported ad hoc query parameters: 

7 

8- ``TYPENAMES`` 

9- ``SRSNAME`` 

10- ``BBOX`` 

11- ``STARTINDEX`` 

12- ``COUNT`` 

13- ``OUTPUTFORMAT`` 

14- ``RESULTTYPE`` 

15 

16@TODO: FILTER, SORTBY 

17 

18Supported stored queries: 

19 

20- ``urn:ogc:def:query:OGC-WFS::GetFeatureById`` 

21 

22For ``GetPropertyValue`` only simple ``VALUEREFERENCE`` (field name) is supported. 

23 

24References: 

25 - OGC 09-025r1 (https://portal.ogc.org/files/?artifact_id=39967) 

26 - https://mapserver.org/ogc/wfs_server.html 

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

28 

29""" 

30 

31import gws 

32import gws.base.ows.server as server 

33import gws.base.shape 

34import gws.base.web 

35import gws.config.util 

36import gws.gis.bounds 

37import gws.gis.crs 

38import gws.lib.metadata 

39import gws.lib.mime 

40 

41gws.ext.new.owsService('wfs') 

42 

43STORED_QUERY_GET_FEATURE_BY_ID = "urn:ogc:def:query:OGC-WFS::GetFeatureById" 

44 

45_cdir = gws.u.dirname(__file__) 

46 

47_DEFAULT_TEMPLATES = [ 

48 gws.Config( 

49 type='py', 

50 path=f'{_cdir}/templates/getCapabilities.cx.py', 

51 subject='ows.GetCapabilities', 

52 mimeTypes=[gws.lib.mime.XML], 

53 ), 

54 gws.Config( 

55 type='py', 

56 path=f'{_cdir}/templates/getFeature3.cx.py', 

57 subject='ows.GetFeature', 

58 access=gws.c.PUBLIC, 

59 mimeTypes=[gws.lib.mime.XML, gws.lib.mime.GML, gws.lib.mime.GML3], 

60 ), 

61 gws.Config( 

62 type='py', 

63 path=f'{_cdir}/templates/getFeatureGeoJson.cx.py', 

64 subject='ows.GetFeature', 

65 access=gws.c.PUBLIC, 

66 mimeTypes=[gws.lib.mime.JSON, gws.lib.mime.GEOJSON], 

67 ), 

68 gws.Config( 

69 type='py', 

70 path=f'{_cdir}/templates/getFeature2.cx.py', 

71 subject='ows.GetFeature', 

72 mimeTypes=[gws.lib.mime.GML2], 

73 ), 

74 gws.Config( 

75 type='py', 

76 path=f'{_cdir}/templates/getPropertyValue.cx.py', 

77 subject='ows.GetPropertyValue', 

78 mimeTypes=[gws.lib.mime.XML, gws.lib.mime.GML, gws.lib.mime.GML3], 

79 ), 

80 gws.Config( 

81 type='py', 

82 path=f'{_cdir}/templates/listStoredQueries.cx.py', 

83 subject='ows.ListStoredQueries', 

84 mimeTypes=[gws.lib.mime.XML], 

85 ), 

86 gws.Config( 

87 type='py', 

88 path=f'{_cdir}/templates/describeStoredQueries.cx.py', 

89 subject='ows.DescribeStoredQueries', 

90 mimeTypes=[gws.lib.mime.XML], 

91 ), 

92] 

93 

94_DEFAULT_METADATA = gws.Metadata( 

95 name='WFS', 

96 inspireMandatoryKeyword='infoMapAccessService', 

97 inspireResourceType='service', 

98 inspireSpatialDataServiceType='view', 

99 isoScope='dataset', 

100 isoServiceFunction='download', 

101 isoSpatialRepresentationType='vector', 

102) 

103 

104 

105class Config(server.service.Config): 

106 """WFS Service configuration""" 

107 pass 

108 

109 

110class Object(server.service.Object): 

111 protocol = gws.OwsProtocol.WFS 

112 supportedVersions = ['2.0.2', '2.0.1', '2.0.0'] 

113 isVectorService = True 

114 isOwsCommon = True 

115 

116 def configure_templates(self): 

117 return gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES) 

118 

119 def configure_metadata(self): 

120 super().configure_metadata() 

121 self.metadata = gws.lib.metadata.merge(_DEFAULT_METADATA, self.metadata) 

122 

123 def configure_operations(self): 

124 self.supportedOperations = [ 

125 gws.OwsOperation( 

126 verb=gws.OwsVerb.DescribeFeatureType, 

127 formats=[gws.lib.mime.GML3], 

128 handlerName='handle_describe_feature_type', 

129 ), 

130 gws.OwsOperation( 

131 verb=gws.OwsVerb.DescribeStoredQueries, 

132 formats=self.available_formats(gws.OwsVerb.DescribeStoredQueries), 

133 handlerName='handle_describe_stored_queries', 

134 ), 

135 gws.OwsOperation( 

136 verb=gws.OwsVerb.GetCapabilities, 

137 formats=self.available_formats(gws.OwsVerb.GetCapabilities), 

138 handlerName='handle_get_capabilities', 

139 ), 

140 gws.OwsOperation( 

141 verb=gws.OwsVerb.GetFeature, 

142 formats=self.available_formats(gws.OwsVerb.GetFeature), 

143 handlerName='handle_get_feature', 

144 ), 

145 gws.OwsOperation( 

146 verb=gws.OwsVerb.GetPropertyValue, 

147 formats=self.available_formats(gws.OwsVerb.GetPropertyValue), 

148 handlerName='handle_get_property_value', 

149 ), 

150 gws.OwsOperation( 

151 verb=gws.OwsVerb.ListStoredQueries, 

152 formats=self.available_formats(gws.OwsVerb.ListStoredQueries), 

153 handlerName='handle_list_stored_queries', 

154 ), 

155 ] 

156 

157 ## 

158 

159 def init_request(self, req): 

160 sr = super().init_request(req) 

161 

162 sr.crs = sr.requested_crs('CRSNAME,SRSNAME') or sr.project.map.bounds.crs 

163 sr.targetCrs = sr.crs 

164 sr.alwaysXY = False 

165 sr.bounds = ( 

166 sr.requested_bounds('BBOX') if sr.req.has_param('BBOX') 

167 else gws.gis.bounds.transform(sr.project.map.bounds, sr.crs) 

168 ) 

169 

170 return sr 

171 

172 def layer_is_suitable(self, layer: gws.Layer): 

173 return not layer.isGroup and layer.isSearchable and layer.ows.xmlNamespace 

174 

175 ## 

176 

177 def handle_get_capabilities(self, sr: server.request.Object): 

178 return self.template_response( 

179 sr, 

180 sr.requested_format('OUTPUTFORMAT'), 

181 layerCapsList=sr.layerCapsList, 

182 ) 

183 

184 def handle_list_stored_queries(self, sr: server.request.Object): 

185 return self.template_response( 

186 sr, 

187 sr.requested_format('FORMAT'), 

188 layerCapsList=sr.layerCapsList, 

189 ) 

190 

191 def handle_describe_stored_queries(self, sr: server.request.Object): 

192 s = sr.string_param('STOREDQUERY_ID', default='') 

193 if s and s != STORED_QUERY_GET_FEATURE_BY_ID: 

194 raise server.error.InvalidParameterValue('STOREDQUERY_ID') 

195 

196 return self.template_response( 

197 sr, 

198 sr.requested_format('OUTPUTFORMAT'), 

199 layerCapsList=sr.layerCapsList, 

200 ) 

201 

202 def handle_describe_feature_type(self, sr: server.request.Object): 

203 lcs = self.requested_layer_caps(sr) 

204 xml = server.layer_caps.xml_schema(lcs, sr.req.user) 

205 return gws.ContentResponse( 

206 mime=gws.lib.mime.XML, 

207 content=xml.to_string( 

208 with_xml_declaration=True, 

209 with_namespace_declarations=True, 

210 with_schema_locations=True, 

211 ) 

212 ) 

213 

214 def handle_get_feature(self, sr: server.request.Object): 

215 fc = self.get_features(sr) 

216 return self.template_response( 

217 sr, 

218 sr.requested_format('OUTPUTFORMAT'), 

219 featureCollection=fc 

220 ) 

221 

222 def handle_get_property_value(self, sr: server.request.Object): 

223 value_ref = sr.string_param('VALUEREFERENCE') 

224 fc = self.get_features(sr) 

225 fc.values = [m.feature.get(value_ref) for m in fc.members] 

226 return self.template_response( 

227 sr, 

228 sr.requested_format('OUTPUTFORMAT'), 

229 featureCollection=fc 

230 ) 

231 

232 ## 

233 

234 def requested_layer_caps(self, sr: server.request.Object): 

235 lcs = [] 

236 for name in sr.list_param('TYPENAME,TYPENAMES'): 

237 for lc in sr.layerCapsList: 

238 if server.layer_caps.feature_name_matches(lc, name): 

239 lcs.append(lc) 

240 if not lcs: 

241 raise server.error.LayerNotDefined() 

242 return gws.u.uniq(lcs) 

243 

244 SEARCH_MAX_TOTAL = 100_000 

245 

246 def get_features(self, sr: server.request.Object, value_ref: str = '') -> server.FeatureCollection: 

247 

248 # @TODO optimize paging for db-based layers 

249 

250 lcs = self.requested_layer_caps(sr) 

251 search = self.make_search(sr, lcs) 

252 

253 results = self.root.app.searchMgr.run_search(search, sr.req.user) 

254 

255 if value_ref: 

256 results = [r for r in results if r.feature.has(value_ref)] 

257 

258 hits = len(results) 

259 

260 result_type = sr.string_param('RESULTTYPE', values={'hits', 'results'}, default='results') 

261 if result_type == 'hits': 

262 return self.feature_collection(sr, lcs, hits, []) 

263 

264 limit = sr.requested_feature_count('COUNT,MAXFEATURES') 

265 offset = sr.int_param('STARTINDEX', default=0) 

266 

267 if offset: 

268 results = results[offset:] 

269 if limit: 

270 results = results[:limit] 

271 

272 return self.feature_collection(sr, lcs, hits, results) 

273 

274 def make_search(self, sr: server.request.Object, lcs): 

275 search = gws.SearchQuery( 

276 project=sr.project, 

277 layers=[lc.layer for lc in lcs], 

278 limit=self.SEARCH_MAX_TOTAL, 

279 ) 

280 

281 s = sr.string_param('STOREDQUERY_ID', default='') 

282 if s: 

283 if s != STORED_QUERY_GET_FEATURE_BY_ID: 

284 raise server.error.InvalidParameterValue('STOREDQUERY_ID') 

285 uid = sr.string_param('id') 

286 search.uids = [uid] 

287 return search 

288 

289 # @TODO filters 

290 # flt: Optional[gws.SearchFilter] = None 

291 # if sr.req.has_param('filter'): 

292 # src = sr.req.param('filter') 

293 # try: 

294 # flt = gws.gis.ows.filter.from_fes_string(src) 

295 # except gws.gis.ows.filter.Error as err: 

296 # gws.log.error(f'FILTER ERROR: {err!r} filter={src!r}') 

297 # raise gws.base.web.error.BadRequest('Invalid FILTER value') 

298 

299 search.shape = gws.base.shape.from_bounds(sr.bounds) 

300 return search