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 22:59 +0200

1"""OGC fes 2.0 filter 

2 

3Supports 

4 

5 - Minimum Standard Filter 

6 PropertyIsEqualTo, PropertyIsNotEqualTo, PropertyIsLessThan, PropertyIsGreaterThan, 

7 PropertyIsLessThanOrEqualTo, PropertyIsGreaterThanOrEqualTo. 

8 Implements the logical operators. Does not implement any additional functions. 

9 

10 - Minimum Spatial Filter 

11 Implements only the BBOX spatial operator. 

12 

13References: 

14 - OGC® Filter Encoding 2.0 Encoding Standard (http://docs.opengeospatial.org/is/09-026r2/09-026r2.html) 

15""" 

16 

17import re 

18import operator 

19 

20import gws 

21import gws.base.shape 

22import gws.lib.bounds 

23import gws.lib.gml 

24import gws.lib.xmlx as xmlx 

25 

26 

27class Error(gws.Error): 

28 pass 

29 

30 

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} 

40 

41 

42## 

43 

44class Matcher: 

45 def get_property(self, obj, prop): 

46 return getattr(obj, prop, None) 

47 

48 def get_shape(self, obj): 

49 return getattr(obj, 'shape', None) 

50 

51 def matches(self, flt: gws.SearchFilter, obj): 

52 return getattr(self, f'match_{flt.operator}'.lower())(flt, obj) 

53 

54 ## 

55 

56 def match_and(self, flt, obj): 

57 return all(self.matches(sf, obj) for sf in flt.subFilters) 

58 

59 def match_or(self, flt, obj): 

60 return any(self.matches(sf, obj) for sf in flt.subFilters) 

61 

62 def match_not(self, flt, obj): 

63 return not (self.matches(flt.subFilters[0], obj)) 

64 

65 ## 

66 

67 def match_propertyisequalto(self, flt, obj): 

68 return self.compare(self.get_property(obj, flt.property), flt.value, operator.eq) 

69 

70 def match_propertyisnotequalto(self, flt, obj): 

71 return self.compare(self.get_property(obj, flt.property), flt.value, operator.ne) 

72 

73 def match_propertyislessthan(self, flt, obj): 

74 return self.compare(self.get_property(obj, flt.property), flt.value, operator.lt) 

75 

76 def match_propertyisgreaterthan(self, flt, obj): 

77 return self.compare(self.get_property(obj, flt.property), flt.value, operator.gt) 

78 

79 def match_propertyislessthanorequalto(self, flt, obj): 

80 return self.compare(self.get_property(obj, flt.property), flt.value, operator.le) 

81 

82 def match_propertyisgreaterthanorequalto(self, flt, obj): 

83 return self.compare(self.get_property(obj, flt.property), flt.value, operator.ge) 

84 

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) 

92 

93 ## 

94 

95 """ 

96 @TODO 

97 Equals 

98 Disjoint 

99 Touches 

100 Within 

101 Overlaps 

102 Crosses 

103 Intersects 

104 Contains 

105 DWithin 

106 Beyond 

107  

108 """ 

109 

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) 

115 

116 

117## 

118 

119 

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) 

126 

127 

128def from_fes_element(el: gws.XmlElement) -> gws.SearchFilter: 

129 op = el.lcName 

130 sub = el.children() 

131 

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]) 

137 

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]) 

144 

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]) 

151 

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]) 

156 

157 if op not in _SUPPORTED_OPS: 

158 raise Error(f'unsupported filter operation {el.name!r}') 

159 

160 flt = gws.SearchFilter( 

161 operator=_SUPPORTED_OPS[op], 

162 ) 

163 

164 # @TODO support "prop = prop" 

165 # @TODO support matchCase, matchAction 

166 

167 v = el.findfirst('ValueReference', 'PropertyName') 

168 if not v or not v.text: 

169 raise Error(f'invalid property name or value reference') 

170 

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) 

176 

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 

184 

185 v = el.findfirst('Literal') 

186 if v: 

187 flt.value = v.text.strip() 

188 return flt 

189 

190 raise Error(f'unsupported filter')