Coverage for gws-app/gws/plugin/gekos/index.py: 0%
92 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"""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 {}), verify=False)
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 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 xy = self._free_point(
115 float(rec.pop('X')),
116 float(rec.pop('Y')),
117 points,
118 )
119 points.add(xy)
121 rec['instance'] = instance_name
123 uid = instance_name + '_' + str(rec['ObjectID'])
124 if uid in uids:
125 gws.log.warning(f'non-unique {uid=} ignored')
126 continue
127 uids.add(uid)
128 rec['uid'] = uid
130 shape = gws.lib.shape.from_geojson(
131 {'type': 'Point', 'coordinates': xy},
132 self.crs,
133 )
134 rec['wkb_geometry'] = shape.to_ewkb_hex()
136 recs2.append(rec)
138 return recs2
140 def _free_point(self, x, y, points):
141 """Apply the position correction to a point."""
143 if not self.position:
144 return x, y
146 # move a point by specified offsets
148 x = round(x, 3) + self.position.offsetX
149 y = round(y, 3) + self.position.offsetY
151 if (x, y) not in points:
152 return x, y
154 # if two or more points share the same XY,
155 # arrange them in a circle around XY
157 distance = self.position.distance
158 angle = self.position.angle
160 if not distance:
161 return x, y
163 for a in range(0, 360, angle):
164 a = math.radians(a)
165 xa = round(x + distance * math.cos(a))
166 ya = round(y + distance * math.sin(a))
168 if (xa, ya) not in points:
169 return xa, ya
171 return x, y
173 def _write(self, recs):
174 """Recreate the index table and insert the records."""
175 columns = [
176 sa.Column('uid', sa.Text, primary_key=True),
177 sa.Column('ObjectID', sa.Text),
178 sa.Column('AntragsartBez', sa.Text),
179 sa.Column('AntragsartID', sa.Integer, index=True),
180 sa.Column('Darstellung', sa.Text),
181 sa.Column('Massnahme', sa.Text),
182 sa.Column('SystemNr', sa.Text),
183 sa.Column('status', sa.Text),
184 sa.Column('Tooltip', sa.Text),
185 sa.Column('UrlFV', sa.Text),
186 sa.Column('UrlOL', sa.Text),
187 sa.Column('Verfahren', sa.Text),
188 sa.Column('instance', sa.Text),
189 sa.Column('wkb_geometry', sa.geo.Geometry(geometry_type='POINT', srid=self.crs.srid), index=True),
190 ]
192 schema, name = self.db.split_table_name(self.tableName)
193 sa_meta = sa.MetaData(schema=schema)
194 table = sa.Table(name, sa_meta, *columns, schema=schema)
196 with self.db.connect() as conn:
197 table.drop(conn.saConn, checkfirst=True)
198 table.create(conn.saConn)
199 conn.commit()
200 conn.execute(sa.insert(table).values(recs))
201 conn.commit()
203 gws.log.info(f'saved {len(recs)} records in {schema}.{name}')