Coverage for gws-app/gws/plugin/ows_client/wfs/provider.py: 0%
74 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""WFS service provider."""
3from typing import Optional, cast
5import gws
6import gws.base.ows.client
7import gws.lib.shape
8import gws.config.util
9import gws.lib.bounds
10import gws.lib.crs
11import gws.lib.extent
12import gws.gis.source
14from . import caps
17class Config(gws.base.ows.client.provider.Config):
18 """Connection to a WFS service."""
20 withBboxCrs: Optional[bool]
21 """Add the CRS to the bounding box parameter."""
24class Object(gws.base.ows.client.provider.Object):
25 """WFS service provider.
27 Reads the capabilities of a WFS service and runs GetFeature requests for
28 the layers, finders and models that use the service.
29 """
31 protocol = gws.OwsProtocol.WFS
32 withBboxCrs: bool
33 """Append the CRS to the ``BBOX`` parameter. Defaults to ``True`` for WFS 2."""
34 isWfs2: bool
35 """The service version is 2 or higher."""
37 def configure(self):
38 cc = caps.parse(self.get_capabilities())
40 self.metadata = cc.metadata
41 self.sourceLayers = cc.sourceLayers
42 self.version = cc.version
43 self.isWfs2 = self.version >= '2'
45 self.configure_operations(cc.operations)
47 # use bbox with crs for wfs 2 by default
48 # see also comments in qgis/qgswfsfeatureiterator.cpp buildURL
49 p = self.cfg('withBboxCrs')
50 self.withBboxCrs = self.isWfs2 if p is None else p
52 DEFAULT_GET_FEATURE_LIMIT = 100
53 """Max. number of features in GetFeature requests, if the search has no limit."""
55 def create_leaf_layer_config(self, source_layers):
56 """Create the configuration of a ``wfsflat`` layer for the given feature types.
58 Used by the ``wfs`` tree layer to create its child layers.
60 Args:
61 source_layers: Feature types to show in the layer.
63 Returns:
64 A layer configuration that uses this provider.
65 """
66 return dict(
67 type='wfsflat',
68 _defaultProvider=self,
69 _defaultSourceLayers=source_layers,
70 )
72 def get_features(self, search, source_layers):
73 bounds = search.bounds
74 search_shape = None
76 if search.shape:
77 geometry_tolerance = 0.0
79 if search.tolerance:
80 n, u = search.tolerance
81 geometry_tolerance = n * (search.resolution or 1) if u == 'px' else n
83 search_shape = search.shape.tolerance_polygon(geometry_tolerance)
84 bounds = search_shape.bounds()
86 if not bounds:
87 gws.log.warning('get_features: no bounds or shape given')
88 return []
90 request_crs = self.forceCrs or gws.lib.crs.WGS84
92 bbox = gws.lib.bounds.transform(bounds, request_crs).extent
93 if request_crs.isYX and not self.alwaysXY:
94 bbox = gws.lib.extent.swap_xy(bbox)
95 bbox = ','.join(str(k) for k in bbox)
97 srs = request_crs.urn
98 if self.withBboxCrs:
99 bbox += ',' + srs
101 params = {
102 'BBOX': bbox,
103 'COUNT' if self.isWfs2 else 'MAXFEATURES': search.limit or self.DEFAULT_GET_FEATURE_LIMIT,
104 'SRSNAME': srs,
105 'TYPENAMES' if self.isWfs2 else 'TYPENAME': [sl.name for sl in source_layers],
106 'VERSION': self.version,
107 }
109 if search.extraParams:
110 params = gws.u.merge(params, gws.u.to_upper_dict(search.extraParams))
112 op = self.get_operation(gws.OwsVerb.GetFeature)
113 if not op:
114 return []
116 if op.preferredFormat:
117 params.setdefault('OUTPUTFORMAT', op.preferredFormat)
119 args = self.prepare_operation(op, params=params)
120 text = gws.base.ows.client.request.get_text(args)
122 try:
123 records = gws.base.ows.client.featureinfo.parse(text, default_crs=request_crs, always_xy=self.alwaysXY)
124 except gws.Error as exc:
125 gws.log.error(f'get_features: parse error: {exc!r}')
126 return []
128 gws.log.debug(f'get_features: FOUND={len(records)} params={params!r}')
130 for rec in records:
131 if rec.shape:
132 rec.shape = rec.shape.transformed_to(bounds.crs)
134 if not search_shape:
135 return records
137 filtered = [
138 rec for rec in records
139 if not rec.shape or rec.shape.intersects(search_shape)
140 ]
142 gws.log.debug(f'get_features: FILTERED={len(filtered)}')
143 return filtered