Coverage for gws-app/gws/plugin/gekos/index.py: 0%
94 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""GekoS index, loaded from gek-online."""
4import math
6import gws
7import gws.lib.shape
8import gws.config.util
9import gws.lib.crs
10import gws.lib.xmlx
11import gws.lib.net
12import gws.lib.sa as sa
13from . import core
16"""
17Gekos-Online can be called with different "instance" parameters or no "instance" at all
19The xml structure is like this:
21 <?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
22 <OnlineTreffer>
23 <Vorgang>
24 <AntragsartID>..</AntragsartID>
25 <SystemNr>...</SystemNr>
26 <X>407000.000</X>
27 <Y>5716000.000</Y>
28 <ObjectID>1</ObjectID>
29 <Verfahren>...</Verfahren>
30 <AntragsartBez>...</AntragsartBez>
31 <Darstellung>...</Darstellung>
32 <Massnahme>....</Massnahme>
33 <Tooltip>...</Tooltip>
34 <UrlFV>...</UrlFV>
35 <UrlOL>...</UrlOL>
36 </Vorgang>
37 <Vorgang>
38 ....
40ObjectID appears to be unique within an instance, so we generate a PK = instance_ObjectID
42"""
45class Object(gws.Node):
46 """GekoS index.
48 Loads GekoS records from gek-online and stores them in a PostGIS table.
49 """
51 db: gws.DatabaseProvider
52 """Database provider for the index table."""
53 tableName: str
54 """Name of the index table."""
55 position: core.PositionConfig
56 """Position correction for points, or ``None``."""
57 crs: gws.Crs
58 """CRS of the GekoS coordinates."""
60 def configure(self):
61 gws.config.util.configure_database_provider_for(self, ext_type='postgres')
62 self.tableName = self.cfg('tableName')
63 self.crs = gws.lib.crs.get(self.cfg('crs'))
64 self.position = self.cfg('position')
66 def create(self):
67 """Load the records of all sources and recreate the index table.
69 The table is dropped, created again and filled with the records.
70 """
71 recs = self._collect()
72 self._write(recs)
74 def _collect(self):
75 """Load and transform the records of all sources."""
76 recs = []
78 for source in self.cfg('sources'):
79 rs = self._load(source)
80 gws.log.info(f'loaded {len(rs)} records from {source.instance!r}')
81 rs = self._transform(rs, source.instance)
82 recs.extend(rs)
84 return recs
86 def _load(self, source: core.SourceConfig):
87 """Load the records of a gek-online source as dicts."""
89 res = gws.lib.net.http_request(source.url, params=dict(source.params or {}))
90 res.raise_if_failed()
91 xml = gws.lib.xmlx.from_string((res.text or '').strip(), gws.XmlOptions(removeNamespaces=True))
93 rs = []
95 for node in xml.findall('Vorgang'):
96 rec = {}
97 for tag in node:
98 rec[tag.name] = tag.text
99 rs.append(rec)
101 return rs
103 def _transform(self, recs, instance_name):
104 """Add uids and point geometries to records, skipping records without coordinates or ``ObjectID`` and duplicate uids."""
106 recs2 = []
107 points = set()
108 uids = set()
110 for rec in recs:
111 if 'X' not in rec or 'Y' not in rec:
112 continue
114 if 'ObjectID' not in rec:
115 gws.log.warning(f'record without ObjectID ignored in {instance_name=}')
116 continue
118 xy = self._free_point(
119 float(rec.pop('X')),
120 float(rec.pop('Y')),
121 points,
122 )
123 points.add(xy)
125 rec['instance'] = instance_name
127 uid = instance_name + '_' + str(rec['ObjectID'])
128 if uid in uids:
129 gws.log.warning(f'non-unique {uid=} ignored')
130 continue
131 uids.add(uid)
132 rec['uid'] = uid
134 shape = gws.lib.shape.from_geojson(
135 {'type': 'Point', 'coordinates': xy},
136 self.crs,
137 )
138 rec['wkb_geometry'] = shape.to_ewkb_hex()
140 recs2.append(rec)
142 return recs2
144 def _free_point(self, x, y, points):
145 """Apply the position correction to a point."""
147 if not self.position:
148 return x, y
150 # move a point by specified offsets
152 x = round(x, 3) + self.position.offsetX
153 y = round(y, 3) + self.position.offsetY
155 if (x, y) not in points:
156 return x, y
158 # if two or more points share the same XY,
159 # arrange them in a circle around XY
161 distance = self.position.distance
162 angle = self.position.angle
164 if not distance or not angle:
165 return x, y
167 for a in range(0, 360, angle):
168 a = math.radians(a)
169 xa = round(x + distance * math.cos(a), 3)
170 ya = round(y + distance * math.sin(a), 3)
172 if (xa, ya) not in points:
173 return xa, ya
175 return x, y
177 def _write(self, recs):
178 """Recreate the index table and insert the records."""
179 columns = [
180 sa.Column('uid', sa.Text, primary_key=True),
181 sa.Column('ObjectID', sa.Text),
182 sa.Column('AntragsartBez', sa.Text),
183 sa.Column('AntragsartID', sa.Integer, index=True),
184 sa.Column('Darstellung', sa.Text),
185 sa.Column('Massnahme', sa.Text),
186 sa.Column('SystemNr', sa.Text),
187 sa.Column('status', sa.Text),
188 sa.Column('Tooltip', sa.Text),
189 sa.Column('UrlFV', sa.Text),
190 sa.Column('UrlOL', sa.Text),
191 sa.Column('Verfahren', sa.Text),
192 sa.Column('instance', sa.Text),
193 sa.Column('wkb_geometry', sa.geo.Geometry(geometry_type='POINT', srid=self.crs.srid), index=True),
194 ]
196 schema, name = self.db.split_table_name(self.tableName)
197 sa_meta = sa.MetaData(schema=schema)
198 table = sa.Table(name, sa_meta, *columns, schema=schema)
200 with self.db.begin() as conn:
201 table.drop(conn.saConn, checkfirst=True)
202 table.create(conn.saConn)
203 if recs:
204 conn.execute(sa.insert(table), recs)
206 gws.log.info(f'saved {len(recs)} records in {schema}.{name}')