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

93 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +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.database.util 

28import gws.base.model.scalar_field 

29import gws.lib.shape 

30import gws.lib.crs 

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 model = cast(gws.base.database.model.Object, self.model) 

132 col = model.column(self.name) 

133 mc.dbSelect.geometryWhere.append(gws.base.database.util.shape_intersects_clause(col, shape, self.geometryCrs)) 

134 

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

136 # here, value is a geosa WKBElement 

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

138 

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

140 shape = self._prop_to_shape(value) 

141 if shape: 

142 return shape.transformed_to(self.geometryCrs) 

143 return gws.ErrorValue 

144 

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

146 return value.to_ewkb_hex() 

147 

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

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

150 

151 def _prop_to_shape(self, value): 

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

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

154 return value 

155 if gws.u.is_data_object(value): 

156 try: 

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

158 except gws.Error: 

159 pass 

160 if gws.u.is_dict(value): 

161 try: 

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

163 except gws.Error: 

164 pass