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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Geometry field.
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.
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.
13Example::
15 fields+ {
16 name "geom"
17 type "geometry"
18 geometryType "polygon"
19 crs "EPSG:25832"
20 }
21"""
23from typing import Optional, cast
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
33@gws.ext.config.modelField('geometry')
34class Config(gws.base.model.scalar_field.Config):
35 """Field for geometries."""
37 geometryType: Optional[gws.GeometryType]
38 """Geometry type."""
39 crs: Optional[gws.CrsName]
40 """CRS of the geometries."""
43@gws.ext.props.modelField('geometry')
44class Props(gws.base.model.scalar_field.Props):
45 geometryType: gws.GeometryType
48@gws.ext.object.modelField('geometry')
49class Object(gws.base.model.scalar_field.Object):
50 """Geometry field object."""
52 attributeType = gws.AttributeType.geometry
53 supportsGeometrySearch = True
55 geometryType: gws.GeometryType
56 """Geometry type of the field."""
57 geometryCrs: gws.Crs
58 """CRS of the stored geometries."""
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}')
69 def configure_geometry_type(self):
70 """Set the geometry type from the config or from the database column.
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
80 col = self.describe()
81 if col and col.geometryType:
82 self.geometryType = col.geometryType
83 return True
85 def configure_geometry_crs(self):
86 """Set the CRS from the config or from the SRID of the database column.
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
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
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
108 ##
110 def props(self, user):
111 return gws.u.merge(super().props(user), geometryType=self.geometryType)
113 ##
115 def before_select(self, mc):
116 super().before_select(mc)
118 shape = None
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)
130 if shape:
131 shape = shape.transformed_to(self.geometryCrs)
133 model = cast(gws.base.database.model.Object, self.model)
134 col = model.column(self.name)
136 mc.dbSelect.geometryWhere.append(sa.func.st_intersects(col, sa.cast(shape.to_ewkb_hex(), sa.geo.Geometry())))
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))
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
148 def python_to_raw(self, feature, value, mc):
149 return value.to_ewkb_hex()
151 def python_to_prop(self, feature, value, mc):
152 return cast(gws.Shape, value).to_props()
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