Coverage for gws-app/gws/base/search/filter.py: 29%
98 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1"""OGC fes 2.0 filter
3Supports
5 - Minimum Standard Filter
6 PropertyIsEqualTo, PropertyIsNotEqualTo, PropertyIsLessThan, PropertyIsGreaterThan,
7 PropertyIsLessThanOrEqualTo, PropertyIsGreaterThanOrEqualTo.
8 Implements the logical operators. Does not implement any additional functions.
10 - Minimum Spatial Filter
11 Implements only the BBOX spatial operator.
13References:
14 - OGC® Filter Encoding 2.0 Encoding Standard (http://docs.opengeospatial.org/is/09-026r2/09-026r2.html)
15"""
17import re
18import operator
20import gws
21import gws.base.shape
22import gws.lib.bounds
23import gws.lib.gml
24import gws.lib.xmlx as xmlx
27class Error(gws.Error):
28 pass
31_SUPPORTED_OPS = {
32 'propertyisequalto': gws.SearchFilterOperator.PropertyIsEqualTo,
33 'propertyisnotequalto': gws.SearchFilterOperator.PropertyIsNotEqualTo,
34 'propertyislessthan': gws.SearchFilterOperator.PropertyIsLessThan,
35 'propertyisgreaterthan': gws.SearchFilterOperator.PropertyIsGreaterThan,
36 'propertyislessthanorequalto': gws.SearchFilterOperator.PropertyIsLessThanOrEqualTo,
37 'propertyisgreaterthanorequalto': gws.SearchFilterOperator.PropertyIsGreaterThanOrEqualTo,
38 'bbox': gws.SearchFilterOperator.BBOX,
39}
42##
44class Matcher:
45 def get_property(self, obj, prop):
46 return getattr(obj, prop, None)
48 def get_shape(self, obj):
49 return getattr(obj, 'shape', None)
51 def matches(self, flt: gws.SearchFilter, obj):
52 return getattr(self, f'match_{flt.operator}'.lower())(flt, obj)
54 ##
56 def match_and(self, flt, obj):
57 return all(self.matches(sf, obj) for sf in flt.subFilters)
59 def match_or(self, flt, obj):
60 return any(self.matches(sf, obj) for sf in flt.subFilters)
62 def match_not(self, flt, obj):
63 return not (self.matches(flt.subFilters[0], obj))
65 ##
67 def match_propertyisequalto(self, flt, obj):
68 return self.compare(self.get_property(obj, flt.property), flt.value, operator.eq)
70 def match_propertyisnotequalto(self, flt, obj):
71 return self.compare(self.get_property(obj, flt.property), flt.value, operator.ne)
73 def match_propertyislessthan(self, flt, obj):
74 return self.compare(self.get_property(obj, flt.property), flt.value, operator.lt)
76 def match_propertyisgreaterthan(self, flt, obj):
77 return self.compare(self.get_property(obj, flt.property), flt.value, operator.gt)
79 def match_propertyislessthanorequalto(self, flt, obj):
80 return self.compare(self.get_property(obj, flt.property), flt.value, operator.le)
82 def match_propertyisgreaterthanorequalto(self, flt, obj):
83 return self.compare(self.get_property(obj, flt.property), flt.value, operator.ge)
85 def compare(self, a, b, op):
86 if a is None:
87 return False
88 if isinstance(a, list):
89 # @TODO matchAction
90 return any(op(x, b) for x in a)
91 return op(a, b)
93 ##
95 """
96 @TODO
97 Equals
98 Disjoint
99 Touches
100 Within
101 Overlaps
102 Crosses
103 Intersects
104 Contains
105 DWithin
106 Beyond
108 """
110 def match_bbox(self, flt, obj):
111 shape = self.get_shape(obj)
112 if not shape:
113 return False
114 return shape.intersects(flt.shape)
117##
120def from_fes_string(src: str) -> gws.SearchFilter:
121 try:
122 el = xmlx.from_string(src, gws.XmlOptions(removeNamespaces=True))
123 except Exception as exc:
124 raise Error('invalid XML') from exc
125 return from_fes_element(el)
128def from_fes_element(el: gws.XmlElement) -> gws.SearchFilter:
129 op = el.lcName
130 sub = el.children()
132 if op == 'filter':
133 # root element, only allow a single child predicate
134 if len(sub) != 1:
135 raise Error(f'invalid root predicate')
136 return from_fes_element(sub[0])
138 if op == 'and':
139 if len(sub) == 0:
140 raise Error(f'invalid and predicate')
141 if len(sub) == 1:
142 return from_fes_element(sub[0])
143 return gws.SearchFilter(operator=gws.SearchFilterOperator.And, subFilters=[from_fes_element(s) for s in sub])
145 if op == 'or':
146 if len(sub) == 0:
147 raise Error(f'invalid or predicate')
148 if len(sub) == 1:
149 return from_fes_element(sub[0])
150 return gws.SearchFilter(operator=gws.SearchFilterOperator.Or, subFilters=[from_fes_element(s) for s in sub])
152 if op == 'not':
153 if len(sub) != 1:
154 raise Error(f'invalid not predicate')
155 return gws.SearchFilter(operator=gws.SearchFilterOperator.Not, subFilters=[from_fes_element(s) for s in sub])
157 if op not in _SUPPORTED_OPS:
158 raise Error(f'unsupported filter operation {el.name!r}')
160 flt = gws.SearchFilter(
161 operator=_SUPPORTED_OPS[op],
162 )
164 # @TODO support "prop = prop"
165 # @TODO support matchCase, matchAction
167 v = el.findfirst('ValueReference', 'PropertyName')
168 if not v or not v.text:
169 raise Error(f'invalid property name or value reference')
171 # we only support `propName` or `ns:propName`
172 m = re.match(r'^(\w+:)?(\w+)$', v.text)
173 if not m:
174 raise Error(f'invalid property name {v.text!r}')
175 flt.property = m.group(2)
177 if op == 'bbox':
178 v = el.findfirst('Envelope')
179 if not v:
180 raise Error(f'invalid envelope')
181 bounds = gws.lib.gml.parse_envelope(v)
182 flt.shape = gws.base.shape.from_bounds(bounds)
183 return flt
185 v = el.findfirst('Literal')
186 if v:
187 flt.value = v.text.strip()
188 return flt
190 raise Error(f'unsupported filter')