Coverage for gws-app/gws/plugin/model_field/geometry/__init__.py: 84%

94 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Geometry field. 

2 

3A scalar field for geometries, stored as ``gws.Shape`` objects. The geometry 

4type and the CRS are taken from the config or, if not configured, from the 

5database column. A field without a known CRS is a configuration error. 

6 

7The field supports geometry searches: when a model is searched with a shape 

8(optionally widened by a tolerance) or with bounds, the field adds an 

9``ST_Intersects`` condition on its column. Shapes from the client are 

10transformed to the field CRS. Without a configured widget, the field uses a 

11``geometry`` widget. 

12 

13Example:: 

14 

15 fields+ { 

16 name "geom" 

17 type "geometry" 

18 geometryType "polygon" 

19 crs "EPSG:25832" 

20 } 

21""" 

22 

23from typing import Optional, cast 

24 

25import gws 

26import gws.base.database.model 

27import gws.base.model.scalar_field 

28import gws.lib.shape 

29import gws.lib.crs 

30import gws.lib.sa as sa 

31 

32 

33@gws.ext.config.modelField('geometry') 

34class Config(gws.base.model.scalar_field.Config): 

35 """Field for geometries.""" 

36 

37 geometryType: Optional[gws.GeometryType] 

38 """Geometry type.""" 

39 crs: Optional[gws.CrsName] 

40 """CRS of the geometries.""" 

41 

42 

43@gws.ext.props.modelField('geometry') 

44class Props(gws.base.model.scalar_field.Props): 

45 geometryType: gws.GeometryType 

46 

47 

48@gws.ext.object.modelField('geometry') 

49class Object(gws.base.model.scalar_field.Object): 

50 """Geometry field object.""" 

51 

52 attributeType = gws.AttributeType.geometry 

53 supportsGeometrySearch = True 

54 

55 geometryType: gws.GeometryType 

56 """Geometry type of the field.""" 

57 geometryCrs: gws.Crs 

58 """CRS of the stored geometries.""" 

59 

60 def configure(self): 

61 setattr(self, 'geometryType', None) 

62 setattr(self, 'geometryCrs', None) 

63 self.configure_geometry_type() 

64 self.configure_geometry_crs() 

65 if not self.geometryCrs: 

66 tab = getattr(self.model, 'tableName', '') 

67 raise gws.ConfigurationError(f'unknown CRS for {tab!r}.{self.name!r}') 

68 

69 def configure_geometry_type(self): 

70 """Set the geometry type from the config or from the database column. 

71 

72 Returns: 

73 True if the geometry type was set, None otherwise. 

74 """ 

75 s = self.cfg('geometryType') 

76 if s: 

77 self.geometryType = s 

78 return True 

79 

80 col = self.describe() 

81 if col and col.geometryType: 

82 self.geometryType = col.geometryType 

83 return True 

84 

85 def configure_geometry_crs(self): 

86 """Set the CRS from the config or from the SRID of the database column. 

87 

88 Returns: 

89 True if the CRS was set, None otherwise. 

90 """ 

91 s = self.cfg('crs') 

92 if s: 

93 self.geometryCrs = gws.lib.crs.get(s) 

94 return True 

95 

96 col = self.describe() 

97 if col and col.geometrySrid: 

98 crs = gws.lib.crs.get(col.geometrySrid) 

99 if crs: 

100 self.geometryCrs = crs 

101 return True 

102 

103 def configure_widget(self): 

104 if not super().configure_widget(): 

105 self.widget = self.root.create_shared(gws.ext.object.modelWidget, type='geometry') 

106 return True 

107 

108 ## 

109 

110 def props(self, user): 

111 return gws.u.merge(super().props(user), geometryType=self.geometryType) 

112 

113 ## 

114 

115 def before_select(self, mc): 

116 super().before_select(mc) 

117 

118 shape = None 

119 

120 if mc.search.shape: 

121 shape = mc.search.shape 

122 if mc.search.tolerance: 

123 tol_value, tol_unit = mc.search.tolerance 

124 if tol_unit == gws.Uom.px: 

125 tol_value *= mc.search.resolution 

126 shape = shape.tolerance_polygon(tol_value) 

127 elif mc.search.bounds: 

128 shape = gws.lib.shape.from_bounds(mc.search.bounds) 

129 

130 if shape: 

131 shape = shape.transformed_to(self.geometryCrs) 

132 

133 model = cast(gws.base.database.model.Object, self.model) 

134 col = model.column(self.name) 

135 

136 mc.dbSelect.geometryWhere.append(sa.func.st_intersects(col, sa.cast(shape.to_ewkb_hex(), sa.geo.Geometry()))) 

137 

138 def raw_to_python(self, feature, value, mc): 

139 # here, value is a geosa WKBElement 

140 return gws.lib.shape.from_wkb_hex(str(value)) 

141 

142 def prop_to_python(self, feature, value, mc): 

143 shape = self._prop_to_shape(value) 

144 if shape: 

145 return shape.transformed_to(self.geometryCrs) 

146 return gws.ErrorValue 

147 

148 def python_to_raw(self, feature, value, mc): 

149 return value.to_ewkb_hex() 

150 

151 def python_to_prop(self, feature, value, mc): 

152 return cast(gws.Shape, value).to_props() 

153 

154 def _prop_to_shape(self, value): 

155 """Convert a shape, shape props or a dict to a shape, or return None.""" 

156 if isinstance(value, gws.lib.shape.Shape): 

157 return value 

158 if gws.u.is_data_object(value): 

159 try: 

160 return gws.lib.shape.from_props(value) 

161 except gws.Error: 

162 pass 

163 if gws.u.is_dict(value): 

164 try: 

165 return gws.lib.shape.from_dict(value) 

166 except gws.Error: 

167 pass