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
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""WFS Service.
3Implements WFS 2.0 "Basic" profile.
4This implementation only supports ``GET`` requests with ``KVP`` encoding.
6Supported ad hoc query parameters:
8- ``TYPENAMES``
9- ``SRSNAME``
10- ``BBOX``
11- ``STARTINDEX``
12- ``COUNT``
13- ``OUTPUTFORMAT``
14- ``RESULTTYPE``
16@TODO: FILTER, SORTBY
18Supported stored queries:
20- ``urn:ogc:def:query:OGC-WFS::GetFeatureById``
22For ``GetPropertyValue`` only simple ``VALUEREFERENCE`` (field name) is supported.
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
29"""
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
41gws.ext.new.owsService('wfs')
43STORED_QUERY_GET_FEATURE_BY_ID = "urn:ogc:def:query:OGC-WFS::GetFeatureById"
45_cdir = gws.u.dirname(__file__)
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]
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)
105class Config(server.service.Config):
106 """WFS Service configuration"""
107 pass
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
116 def configure_templates(self):
117 return gws.config.util.configure_templates_for(self, extra=_DEFAULT_TEMPLATES)
119 def configure_metadata(self):
120 super().configure_metadata()
121 self.metadata = gws.lib.metadata.merge(_DEFAULT_METADATA, self.metadata)
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 ]
157 ##
159 def init_request(self, req):
160 sr = super().init_request(req)
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 )
170 return sr
172 def layer_is_suitable(self, layer: gws.Layer):
173 return not layer.isGroup and layer.isSearchable and layer.ows.xmlNamespace
175 ##
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 )
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 )
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')
196 return self.template_response(
197 sr,
198 sr.requested_format('OUTPUTFORMAT'),
199 layerCapsList=sr.layerCapsList,
200 )
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 )
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 )
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 )
232 ##
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)
244 SEARCH_MAX_TOTAL = 100_000
246 def get_features(self, sr: server.request.Object, value_ref: str = '') -> server.FeatureCollection:
248 # @TODO optimize paging for db-based layers
250 lcs = self.requested_layer_caps(sr)
251 search = self.make_search(sr, lcs)
253 results = self.root.app.searchMgr.run_search(search, sr.req.user)
255 if value_ref:
256 results = [r for r in results if r.feature.has(value_ref)]
258 hits = len(results)
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, [])
264 limit = sr.requested_feature_count('COUNT,MAXFEATURES')
265 offset = sr.int_param('STARTINDEX', default=0)
267 if offset:
268 results = results[offset:]
269 if limit:
270 results = results[:limit]
272 return self.feature_collection(sr, lcs, hits, results)
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 )
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
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')
299 search.shape = gws.base.shape.from_bounds(sr.bounds)
300 return search