Coverage for gws-app/gws/plugin/alkis/data/indexer.py: 97%
651 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"""Build the ALKIS index from source data."""
3from typing import Optional, Generic, TypeVar
4from collections.abc import Iterator
6import re
8import shapely
9import shapely.strtree
10import shapely.wkb
12import gws
13import gws.lib.osx
14import gws.lib.datetimex as dtx
15from gws.lib.cli import ProgressIndicator
17from . import types as dt
18from . import index
19from . import norbit
20from . import gid
23def run(ix: index.Object, data_schema: str, with_force=False):
24 """Build the ALKIS index.
26 Reads the source tables with the norBIT reader and writes
27 all index tables that do not have data yet. Does nothing if the index is
28 already complete, unless ``with_force`` is set.
30 Args:
31 ix: Index to build.
32 data_schema: Schema with the ALKIS source tables.
33 with_force: Drop all index tables before building.
35 Raises:
36 ``gws.Error``: If the index schema does not exist.
37 """
39 if not ix.has_schema():
40 raise gws.Error(f'ALKIS: schema {ix.schema!r} does not exist')
42 if with_force:
43 ix.drop()
44 elif ix.status().complete:
45 return
47 rdr = norbit.Object(ix.db, schema=data_schema)
48 rr = _Runner(ix, rdr)
49 rr.run()
52##
54T = TypeVar('T')
57class _ObjectDict(Generic[T]):
58 """Collection of entities of one type, by uid."""
60 def __init__(self, cls):
61 """Create an empty collection.
63 Args:
64 cls: Entity class.
65 """
67 self.d = {}
68 self.cls = cls
70 def add(self, uid, recs) -> T:
71 """Create an entity and add it to the collection.
73 The entity is historic if all of its records are.
75 Args:
76 uid: Entity uid.
77 recs: Entity records.
79 Returns:
80 The new entity.
81 """
83 o = self.cls(uid=uid, recs=recs)
84 o.isHistoric = all(r.isHistoric for r in recs)
85 self.d[o.uid] = o
86 return o
88 def get(self, uid, default=None) -> Optional[T]:
89 """Return an entity by uid.
91 Args:
92 uid: Entity uid.
93 default: Value to return if the uid is not found.
95 Returns:
96 The entity or the default value.
97 """
99 return self.d.get(uid, default)
101 def get_many(self, uids) -> list[T]:
102 """Return entities by uids.
104 Unknown uids are skipped, duplicates are returned once.
106 Args:
107 uids: Entity uids.
109 Returns:
110 A list of entities, in the order of ``uids``.
111 """
113 res = {}
115 for uid in uids:
116 if uid not in res:
117 o = self.d.get(uid)
118 if o:
119 res[uid] = o
121 return list(res.values())
123 def get_from_ptr(self, obj: dt.Entity, attr):
124 """Return the entities referenced by an attribute of the records of an entity.
126 The attribute is removed from the records.
128 Args:
129 obj: Entity whose records hold the references.
130 attr: Attribute name, holding a uid or a list of uids.
132 Returns:
133 A list of referenced entities.
134 """
136 uids = []
138 for r in obj.recs:
139 v = _pop(r, attr)
140 if isinstance(v, list):
141 uids.extend(v)
142 elif isinstance(v, str):
143 uids.append(v)
145 return self.get_many(uids)
147 def __iter__(self) -> Iterator[T]:
148 yield from self.d.values()
150 def __len__(self):
151 return len(self.d)
154class _ObjectMap:
155 """Collections of all entities collected by an indexer."""
157 def __init__(self):
158 self.Anschrift: _ObjectDict[dt.Anschrift] = _ObjectDict(dt.Anschrift)
159 self.Buchungsblatt: _ObjectDict[dt.Buchungsblatt] = _ObjectDict(dt.Buchungsblatt)
160 self.Buchungsstelle: _ObjectDict[dt.Buchungsstelle] = _ObjectDict(dt.Buchungsstelle)
161 self.Flurstueck: _ObjectDict[dt.Flurstueck] = _ObjectDict(dt.Flurstueck)
162 self.Gebaeude: _ObjectDict[dt.Gebaeude] = _ObjectDict(dt.Gebaeude)
163 self.Lage: _ObjectDict[dt.Lage] = _ObjectDict(dt.Lage)
164 self.Namensnummer: _ObjectDict[dt.Namensnummer] = _ObjectDict(dt.Namensnummer)
165 self.Part: _ObjectDict[dt.Part] = _ObjectDict(dt.Part)
166 self.Person: _ObjectDict[dt.Person] = _ObjectDict(dt.Person)
168 self.placeAll: dict = {}
169 self.placeIdx: dict = {}
170 self.catalog: dict = {}
173class _Indexer:
174 """Base class for indexers.
176 An indexer collects entities of some kinds from the source data into its
177 object map and writes them into index tables.
178 """
180 def __init__(self, runner: '_Runner'):
181 """Create an indexer.
183 Args:
184 runner: Runner that owns this indexer.
185 """
187 self.rr = runner
188 self.ix: index.Object = runner.ix
189 self.om = _ObjectMap()
191 def collect(self):
192 """Collect entities from the source data into the object map."""
194 pass
196 def write_table(self, table_id, values):
197 """Create and fill an index table, unless it already has data.
199 Args:
200 table_id: Table id.
201 values: Rows as dicts.
202 """
204 if self.ix.has_table(table_id):
205 return
206 with ProgressIndicator(f'ALKIS: write {table_id!r}'):
207 self.ix.create_table(table_id, values)
209 def write(self):
210 """Write the collected entities into the index tables."""
212 pass
215class _PlaceIndexer(_Indexer):
216 """Indexer for places (Administrations- und Verwaltungseinheiten).
218 Collects current Laender, Regierungsbezirke, Kreise, Gemeinden, Gemarkungen,
219 Buchungsblattbezirke and Dienststellen. Place codes are built from the key
220 parts as in the official municipality key, see
221 https://de.wikipedia.org/wiki/Amtlicher_Gemeindeschl%C3%BCssel
222 """
224 empty1 = dt.EnumPair(code='0', text='')
225 """Empty place value for one-digit codes."""
226 empty2 = dt.EnumPair(code='00', text='')
227 """Empty place value for two-digit codes."""
229 def add(self, kind, ax, key_obj, **kwargs):
230 """Add a place, unless it is historic.
232 Args:
233 kind: Place kind, a ``PlaceKind`` value.
234 ax: Source object.
235 key_obj: Key object of the place.
236 **kwargs: Higher level places.
238 Returns:
239 The place value, or ``None`` if the place is historic.
240 """
242 if ax.lebenszeitintervall.endet is not None:
243 return
245 code = self.code(kind, key_obj)
246 value = dt.EnumPair(code, ax.bezeichnung)
248 p = dt.Place(**kwargs)
250 p.uid = kind + code
251 p.kind = kind
252 setattr(p, kind, value)
254 self.om.placeAll[p.uid] = p
255 self.om.placeIdx[p.uid] = value
257 return value
259 def collect(self):
260 self.om.placeAll = {}
261 self.om.placeIdx = {}
263 for ax in self.rr.read_flat(gid.AX_Bundesland):
264 self.add('land', ax, ax.schluessel)
266 for ax in self.rr.read_flat(gid.AX_Regierungsbezirk):
267 o = ax.schluessel
268 self.add('regierungsbezirk', ax, o, land=self.get_land(o))
270 for ax in self.rr.read_flat(gid.AX_KreisRegion):
271 o = ax.schluessel
272 self.add('kreis', ax, o, land=self.get_land(o), regierungsbezirk=self.get_regierungsbezirk(o))
274 for ax in self.rr.read_flat(gid.AX_Gemeinde):
275 o = ax.gemeindekennzeichen
276 self.add('gemeinde', ax, o, land=self.get_land(o), regierungsbezirk=self.get_regierungsbezirk(o), kreis=self.get_kreis(o))
278 # @TODO map Gemarkung to Gemeinde (see https://de.wikipedia.org/wiki/Liste_der_Gemarkungen_in_Nordrhein-Westfalen etc)
280 for ax in self.rr.read_flat(gid.AX_Gemarkung):
281 if str(ax.schluessel.gemarkungsnummer) in self.ix.excludeGemarkung:
282 continue
283 o = ax.schluessel
284 self.add('gemarkung', ax, o, land=self.get_land(o))
286 for ax in self.rr.read_flat(gid.AX_Buchungsblattbezirk):
287 o = ax.schluessel
288 self.add('buchungsblattbezirk', ax, o, land=self.get_land(o))
290 for ax in self.rr.read_flat(gid.AX_Dienststelle):
291 o = ax.schluessel
292 self.add('dienststelle', ax, o, land=self.get_land(o))
294 def write(self):
295 values = []
297 for place in self.om.placeAll.values():
298 values.append(
299 dict(
300 uid=place.uid,
301 data=index.serialize(place),
302 )
303 )
305 self.write_table(index.TABLE_PLACE, values)
307 def get_land(self, o):
308 """Return the Land for a key object.
310 Args:
311 o: Key object.
313 Returns:
314 The place value, or an empty value if not found.
315 """
317 return self.get('land', o) or self.empty2
319 def get_regierungsbezirk(self, o):
320 """Return the Regierungsbezirk for a key object.
322 Args:
323 o: Key object.
325 Returns:
326 The place value, or an empty value if not found.
327 """
329 return self.get('regierungsbezirk', o) or self.empty1
331 def get_kreis(self, o):
332 """Return the Kreis for a key object.
334 Args:
335 o: Key object.
337 Returns:
338 The place value, or an empty value if not found.
339 """
341 return self.get('kreis', o) or self.empty2
343 def get_gemeinde(self, o):
344 """Return the Gemeinde for a key object.
346 Args:
347 o: Key object.
349 Returns:
350 The place value, or an empty value if not found.
351 """
353 return self.get('gemeinde', o) or self.empty1
355 def get_gemarkung(self, o):
356 """Return the Gemarkung for a key object.
358 Args:
359 o: Key object.
361 Returns:
362 The place value, or an empty value if not found.
363 """
365 return self.get('gemarkung', o) or self.empty1
367 def get_buchungsblattbezirk(self, o):
368 """Return the Buchungsblattbezirk for a key object.
370 Args:
371 o: Key object.
373 Returns:
374 The place value, or an empty value if not found.
375 """
377 return self.get('buchungsblattbezirk', o) or self.empty1
379 def get_dienststelle(self, o):
380 """Return the Dienststelle for a key object.
382 Args:
383 o: Key object.
385 Returns:
386 The place value, or an empty value if not found.
387 """
389 return self.get('dienststelle', o) or self.empty1
391 def get(self, kind, o):
392 """Return a place by kind and key object.
394 Args:
395 kind: Place kind.
396 o: Key object.
398 Returns:
399 The place value, or ``None`` if not found.
400 """
402 return self.om.placeIdx.get(kind + self.code(kind, o))
404 def is_empty(self, p: dt.EnumPair):
405 """Check whether a place value is empty.
407 Args:
408 p: Place value.
410 Returns:
411 ``True`` if the code is ``0`` or ``00``.
412 """
414 return p.code == '0' or p.code == '00'
416 CODES = {
417 'land': lambda o: o.land,
418 'regierungsbezirk': lambda o: o.land + (o.regierungsbezirk or '0'),
419 'kreis': lambda o: o.land + (o.regierungsbezirk or '0') + o.kreis,
420 'gemeinde': lambda o: o.land + (o.regierungsbezirk or '0') + o.kreis + o.gemeinde,
421 'gemarkung': lambda o: o.land + o.gemarkungsnummer,
422 'buchungsblattbezirk': lambda o: o.land + o.bezirk,
423 'dienststelle': lambda o: o.land + o.stelle,
424 }
425 """Functions that build a place code from a key object, by place kind."""
427 def code(self, kind, o):
428 """Build a place code.
430 Args:
431 kind: Place kind.
432 o: Key object.
434 Returns:
435 The place code.
436 """
438 return self.CODES[kind](o)
441class _LageIndexer(_Indexer):
442 """Indexer for location designations (Lage) and buildings (Gebaeude).
444 The coordinates of a Lage are taken from its house number label (``AP_PTO``
445 with ``art=HNR``). Buildings are linked to the Lage they point to.
446 Building geometries are not stored.
447 """
449 def collect(self):
450 for ax in self.rr.read_flat(gid.AX_LagebezeichnungKatalogeintrag):
451 self.om.catalog[self.lage_key(ax.schluessel)] = ax.bezeichnung
453 for cls in (gid.AX_LagebezeichnungMitHausnummer, gid.AX_LagebezeichnungOhneHausnummer):
454 for uid, axs in self.rr.read_grouped(cls):
455 self.om.Lage.add(
456 uid,
457 [
458 _from_ax(
459 dt.LageRecord,
460 ax,
461 strasse=self.strasse(ax),
462 hausnummer=index.normalize_hausnummer(ax.hausnummer),
463 )
464 for ax in axs
465 ],
466 )
468 # use the PTO (art=HNR) geometry for lage coordinates
469 # PTO.dientZurDarstellungVon -> lage.uid
471 for pto in self.rr.read_flat(gid.AP_PTO):
472 if pto.lebenszeitintervall.endet is not None:
473 continue
474 art = _pop(pto, 'art') or ''
475 if art.upper() != 'HNR':
476 continue
478 uids = _pop(pto, 'dientZurDarstellungVon')
479 if not uids or not isinstance(uids, list):
480 continue
482 geom = _geom_of(pto)
483 if not geom:
484 continue
486 x = geom.centroid.x
487 y = geom.centroid.y
488 for la in self.om.Lage.get_many(uids):
489 la.x = x
490 la.y = y
492 # read related Gebaeude records
494 atts = _meta_attributes(gid.METADATA['AX_Gebaeude'])
496 for uid, axs in self.rr.read_grouped(gid.AX_Gebaeude):
497 self.om.Gebaeude.add(
498 uid,
499 [
500 _from_ax(
501 dt.GebaeudeRecord,
502 ax,
503 name=', '.join(ax.name) if ax.name else None,
504 amtlicheFlaeche=ax.grundflaeche or 0,
505 props=self.rr.props_from(ax, atts),
506 _zeigtAuf=ax.zeigtAuf,
507 )
508 for ax in axs
509 ],
510 )
512 for ge in self.om.Gebaeude:
513 for r in ge.recs:
514 geom = _geom_of(r)
515 r.geomFlaeche = round(geom.area, 2) if geom else 0
517 # omit Gebaeude geometries for now
518 for ge in self.om.Gebaeude:
519 for r in ge.recs:
520 _pop(r, 'geom')
522 for la in self.om.Lage:
523 la.gebaeudeList = []
525 # AX_Gebaeude.zeigtAuf -> AX_LagebezeichnungMitHausnummer
526 for ge in self.om.Gebaeude:
527 for la in self.om.Lage.get_from_ptr(ge, '_zeigtAuf'):
528 la.gebaeudeList.append(ge)
530 def strasse(self, ax):
531 """Return the street name of a source Lage object.
533 Args:
534 ax: Source Lage object.
536 Returns:
537 The unencoded street name, or the name from the catalog for an encoded one.
538 """
540 if isinstance(ax.lagebezeichnung, str):
541 return ax.lagebezeichnung
542 return self.om.catalog.get(self.lage_key(ax.lagebezeichnung), '')
544 def lage_key(self, r):
545 """Return the catalog key for an encoded location.
547 Args:
548 r: Encoded location or catalog key object.
550 Returns:
551 A string of the key parts joined with commas.
552 """
554 return _comma(
555 [
556 getattr(r, 'land'),
557 getattr(r, 'regierungsbezirk'),
558 getattr(r, 'kreis'),
559 getattr(r, 'gemeinde'),
560 getattr(r, 'lage'),
561 ]
562 )
564 def write(self):
565 values = []
567 for la in self.om.Lage:
568 values.append(
569 dict(
570 uid=la.uid,
571 rc=len(la.recs),
572 data=index.serialize(la),
573 )
574 )
576 self.write_table(index.TABLE_LAGE, values)
579class _BuchungIndexer(_Indexer):
580 """Indexer for land register data.
582 Collects Buchungsblaetter with their Buchungsstellen and Namensnummern,
583 Persons and their Anschriften, and the parent-child relations of
584 Buchungsstellen.
585 """
587 buchungsblattkennzeichenMap: dict[str, dt.Buchungsblatt]
588 """Buchungsblaetter by their identifier."""
590 def __init__(self, runner: '_Runner'):
591 super().__init__(runner)
592 self.buchungsblattkennzeichenMap = {}
594 def collect(self):
595 for uid, axs in self.rr.read_grouped(gid.AX_Anschrift):
596 self.om.Anschrift.add(
597 uid,
598 [
599 _from_ax(
600 dt.AnschriftRecord,
601 ax,
602 ort=ax.ort_AmtlichesOrtsnamensverzeichnis or ax.ort_Post,
603 plz=ax.postleitzahlPostzustellung,
604 telefon=ax.telefon[0] if ax.telefon else None,
605 )
606 for ax in axs
607 ],
608 )
610 for uid, axs in self.rr.read_grouped(gid.AX_Person):
611 self.om.Person.add(
612 uid,
613 [
614 _from_ax(
615 dt.PersonRecord,
616 ax,
617 anrede=ax.anrede.text if ax.anrede else None,
618 _hat=ax.hat,
619 )
620 for ax in axs
621 ],
622 )
624 # AX_Person.hat -> [AX_Anschrift]
625 for pe in self.om.Person:
626 pe.anschriftList = self.om.Anschrift.get_from_ptr(pe, '_hat')
628 for uid, axs in self.rr.read_grouped(gid.AX_Namensnummer):
629 self.om.Namensnummer.add(
630 uid,
631 [_from_ax(dt.NamensnummerRecord, ax, anteil=_anteil(ax), _benennt=ax.benennt, _istBestandteilVon=ax.istBestandteilVon) for ax in axs],
632 )
634 # AX_Namensnummer.benennt -> AX_Person
635 for nn in self.om.Namensnummer:
636 nn.laufendeNummer = nn.recs[-1].laufendeNummerNachDIN1421
637 nn.personList = self.om.Person.get_from_ptr(nn, '_benennt')
639 for uid, axs in self.rr.read_grouped(gid.AX_Buchungsstelle):
640 self.om.Buchungsstelle.add(
641 uid,
642 [
643 _from_ax(
644 dt.BuchungsstelleRecord,
645 ax,
646 anteil=_anteil(ax),
647 _an=ax.an,
648 _zu=ax.zu,
649 _istBestandteilVon=ax.istBestandteilVon,
650 )
651 for ax in axs
652 ],
653 )
655 for bs in self.om.Buchungsstelle:
656 bs.laufendeNummer = bs.recs[-1].laufendeNummer
657 bs.fsUids = []
658 bs.flurstueckskennzeichenList = []
660 for uid, axs in self.rr.read_grouped(gid.AX_Buchungsblatt):
661 self.om.Buchungsblatt.add(
662 uid,
663 [
664 _from_ax(
665 dt.BuchungsblattRecord,
666 ax,
667 buchungsblattbezirk=self.rr.place.get_buchungsblattbezirk(ax.buchungsblattbezirk),
668 )
669 for ax in axs
670 ],
671 )
673 for bb in self.om.Buchungsblatt:
674 bb.buchungsstelleList = []
675 bb.namensnummerList = []
676 bb.buchungsblattkennzeichen = bb.recs[-1].buchungsblattkennzeichen
677 self.buchungsblattkennzeichenMap[bb.buchungsblattkennzeichen] = bb
679 # AX_Buchungsstelle.istBestandteilVon -> AX_Buchungsblatt
680 for bs in self.om.Buchungsstelle:
681 bb_list = self.om.Buchungsblatt.get_from_ptr(bs, '_istBestandteilVon')
682 bs.buchungsblattUids = [bb.uid for bb in bb_list]
683 bs.buchungsblattkennzeichenList = [bb.buchungsblattkennzeichen for bb in bb_list]
684 for bb in bb_list:
685 bb.buchungsstelleList.append(bs)
687 # AX_Namensnummer.istBestandteilVon -> AX_Buchungsblatt
688 for nn in self.om.Namensnummer:
689 bb_list = self.om.Buchungsblatt.get_from_ptr(nn, '_istBestandteilVon')
690 nn.buchungsblattUids = [bb.uid for bb in bb_list]
691 nn.buchungsblattkennzeichenList = [bb.buchungsblattkennzeichen for bb in bb_list]
692 for bb in bb_list:
693 bb.namensnummerList.append(nn)
695 for bb in self.om.Buchungsblatt:
696 bb.buchungsstelleList.sort(key=_sortkey_buchungsstelle)
697 bb.namensnummerList.sort(key=_sortkey_namensnummer)
699 # AX_Buchungsstelle.an -> [AX_Buchungsstelle]
700 # AX_Buchungsstelle.zu -> [AX_Buchungsstelle]
701 # see Erläuterungen zu ALKIS Version 6, page 116-119
703 for bs in self.om.Buchungsstelle:
704 bs.childUids = []
705 bs.parentUids = []
706 bs.parentkennzeichenList = []
708 for bs in self.om.Buchungsstelle:
709 parent_uids = set()
710 parent_knz = set()
712 for r in bs.recs:
713 parent_uids.update(_pop(r, '_an'))
714 parent_uids.update(_pop(r, '_zu'))
716 for parent_bs in self.om.Buchungsstelle.get_many(parent_uids):
717 parent_bs.childUids.append(bs.uid)
718 bs.parentUids.append(parent_bs.uid)
719 for bb_knz in parent_bs.buchungsblattkennzeichenList:
720 parent_knz.add(bb_knz + '.' + parent_bs.laufendeNummer)
722 bs.parentkennzeichenList = sorted(parent_knz)
724 def write(self):
725 values = []
727 for bb in self.om.Buchungsblatt:
728 values.append(
729 dict(
730 uid=bb.uid,
731 rc=len(bb.recs),
732 data=index.serialize(bb),
733 )
734 )
736 self.write_table(index.TABLE_BUCHUNGSBLATT, values)
739class _PartIndexer(_Indexer):
740 """Indexer for Parts (Nutzung, Festlegung, Bewertung).
742 Reads all object types with a geometry in the Part categories and
743 intersects their geometries with the most recent Flurstueck geometries.
744 Intersections smaller than ``MIN_PART_AREA`` are skipped.
745 """
747 MIN_PART_AREA = 1
748 """Minimum area of an intersection."""
750 parts: list[dt.Part]
751 """Computed parts."""
753 fs_list: list[dt.Flurstueck]
754 """Flurstuecke, in the order of ``fs_geom``."""
755 fs_geom: list
756 """Most recent Flurstueck geometries."""
758 stree: shapely.strtree.STRtree
759 """Spatial index of ``fs_geom``."""
761 def __init__(self, runner: '_Runner'):
762 super().__init__(runner)
763 self.parts = []
764 self.fs_list = []
765 self.fs_geom = []
767 def collect(self):
769 for kind in dt.Part.KIND:
770 self.collect_kind(kind)
772 for fs in self.rr.fsdata.om.Flurstueck:
773 self.fs_list.append(fs)
774 # NB take only the most recent fs geometry into account
775 self.fs_geom.append(_geom_of(fs.recs[-1]))
777 self.stree = shapely.strtree.STRtree(self.fs_geom)
779 with ProgressIndicator(f'ALKIS: parts', len(self.om.Part)) as progress:
780 for pa in self.om.Part:
781 self.compute_intersections(pa)
782 progress.update(1)
784 self.parts.sort(key=_sortkey_part)
786 for pa in self.parts:
787 pa.isHistoric = all(r.isHistoric for r in pa.recs)
789 def collect_kind(self, kind):
790 """Collect source objects of all object types of a Part kind.
792 Args:
793 kind: Part kind.
794 """
796 _, key = dt.Part.KIND[kind]
797 classes = [
798 getattr(gid, meta['name']) for meta in gid.METADATA.values() if (meta['kind'] == 'object' and meta['geom'] and key + '/' in meta['key'])
799 ]
801 for cls in classes:
802 self.collect_class(kind, cls)
804 def collect_class(self, kind, cls):
805 """Collect source objects of one object type.
807 Args:
808 kind: Part kind.
809 cls: GeoInfoDok class from ``gid``.
810 """
812 meta = gid.METADATA[cls.__name__]
813 atts = _meta_attributes(meta)
815 for uid, axs in self.rr.read_grouped(cls):
816 pa = self.om.Part.add(
817 uid,
818 [
819 _from_ax(
820 dt.PartRecord,
821 ax,
822 props=self.rr.props_from(ax, atts),
823 )
824 for ax in axs
825 ],
826 )
827 pa.kind = kind
828 pa.name = dt.EnumPair(meta['uid'], meta['title'])
830 def compute_intersections(self, pa: dt.Part):
831 """Intersect a source object with the Flurstuecke and add the resulting parts.
833 Args:
834 pa: Source object as a Part.
835 """
837 parts_map = {}
839 for r in pa.recs:
840 geom = _geom_of(r)
841 if not geom:
842 continue
844 for i in self.stree.query(geom):
845 part_geom = shapely.intersection(self.fs_geom[i], geom)
846 part_area = round(part_geom.area, 2)
847 if part_area < self.MIN_PART_AREA:
848 continue
850 fs = self.fs_list[i]
852 part = parts_map.setdefault(
853 fs.uid,
854 dt.Part(
855 uid=pa.uid,
856 recs=[],
857 kind=pa.kind,
858 name=pa.name,
859 fs=fs.uid,
860 ),
861 )
863 # computed area corrected with respect to FS's "amtlicheFlaeche"
864 part_area_corrected = round(fs.recs[-1].amtlicheFlaeche * (part_area / fs.recs[-1].geomFlaeche), 2)
866 part.recs.append(
867 dt.PartRecord(
868 uid=r.uid,
869 beginnt=r.beginnt,
870 endet=r.endet,
871 anlass=r.anlass,
872 props=r.props,
873 geomFlaeche=part_area,
874 amtlicheFlaeche=part_area_corrected,
875 isHistoric=r.endet is not None,
876 )
877 )
879 part.geom = shapely.wkb.dumps(part_geom, srid=self.ix.crs.srid, hex=True)
880 part.geomFlaeche = part_area
881 part.amtlicheFlaeche = part_area_corrected
883 self.parts.extend(parts_map.values())
885 def write(self):
886 values = []
888 for n, pa in enumerate(self.parts, 1):
889 geom = _pop(pa, 'geom')
890 data = index.serialize(pa)
891 pa.geom = geom
892 values.append(
893 dict(
894 n=n,
895 fs=pa.fs,
896 uid=pa.uid,
897 beginnt=pa.recs[-1].beginnt,
898 endet=pa.recs[-1].endet,
899 kind=pa.kind,
900 name=pa.name.text,
901 parthistoric=pa.isHistoric,
902 data=data,
903 geom=geom,
904 )
905 )
907 self.write_table(index.TABLE_PART, values)
910class _FsDataIndexer(_Indexer):
911 """Indexer for Flurstuecke.
913 Collects current and historic Flurstuecke and links them to their Lage,
914 Gebaeude and Buchung data. Flurstuecke without a geometry or with an unknown
915 Gemarkung or Gemeinde are skipped and counted in ``counts``. Also fills in
916 the predecessor lists from the successor lists.
917 """
919 def __init__(self, runner: '_Runner'):
920 super().__init__(runner)
921 self.counts = {
922 'ok': 0,
923 'no_geometry': 0,
924 'excluded': 0,
925 }
927 def collect(self):
928 for uid, axs in self.rr.read_grouped(gid.AX_Flurstueck):
929 recs = gws.u.compact(self.record(ax) for ax in axs)
930 if recs:
931 self.om.Flurstueck.add(uid, recs)
933 for uid, axs in self.rr.read_grouped(gid.AX_HistorischesFlurstueck):
934 recs = gws.u.compact(self.record(ax) for ax in axs)
935 if not recs:
936 continue
937 # For a historic FS, 'beginnt' is basically when the history beginnt
938 # (see comments for AX_HistorischesFlurstueck in gid).
939 # we set F.endet=F.beginnt to designate this one as 'historic'
940 for r in recs:
941 r.endet = r.beginnt
942 r.isHistoric = True
943 self.om.Flurstueck.add(uid, recs)
945 for fs in self.om.Flurstueck:
946 fs.flurstueckskennzeichen = fs.recs[-1].flurstueckskennzeichen
947 fs.fsnummer = index.make_fsnummer(fs.recs[-1])
948 fs.x = fs.recs[-1].x
949 fs.y = fs.recs[-1].y
950 self.process_lage(fs)
951 self.process_gebaeude(fs)
952 self.process_buchung(fs)
954 # check the 'nachfolgerFlurstueckskennzeichen' array
955 # and mark each referenced FS as a "vorgaenger" FS
956 # It is a M:N relation, therefore 'vorgaengerFlurstueckskennzeichen' is also an array
958 knz_to_fs = {fs.flurstueckskennzeichen: fs for fs in self.om.Flurstueck}
959 for fs in self.om.Flurstueck:
960 nfs = fs.recs[-1].nachfolgerFlurstueckskennzeichen
961 if not nfs:
962 continue
963 for nf_knz in nfs:
964 nf_fs = knz_to_fs.get(nf_knz)
965 if not nf_fs:
966 gws.log.warning(f'ALKIS: nachfolgerFlurstueck missing {fs.flurstueckskennzeichen!r}->{nf_knz!r}')
967 continue
968 if not nf_fs.recs[-1].vorgaengerFlurstueckskennzeichen:
969 nf_fs.recs[-1].vorgaengerFlurstueckskennzeichen = []
970 nf_fs.recs[-1].vorgaengerFlurstueckskennzeichen.append(fs.flurstueckskennzeichen)
972 def record(self, ax):
973 """Create a Flurstueck record from a source object.
975 Args:
976 ax: Source Flurstueck object.
978 Returns:
979 The record, or ``None`` if the Flurstueck has no geometry or an unknown Gemarkung or Gemeinde.
980 """
982 r: dt.FlurstueckRecord = _from_ax(
983 dt.FlurstueckRecord,
984 ax,
985 amtlicheFlaeche=ax.amtlicheFlaeche or 0,
986 flurnummer=_str(ax.flurnummer),
987 zaehler=_str(ax.flurstuecksnummer.zaehler),
988 nenner=_str(ax.flurstuecksnummer.nenner),
989 zustaendigeStelle=[self.rr.place.get_dienststelle(p) for p in (ax.zustaendigeStelle or [])],
990 _weistAuf=ax.weistAuf,
991 _zeigtAuf=ax.zeigtAuf,
992 _istGebucht=ax.istGebucht,
993 _buchung=ax.buchung,
994 )
996 # basic data
998 r.gemarkung = self.rr.place.get_gemarkung(ax.gemarkung)
999 r.gemeinde = self.rr.place.get_gemeinde(ax.gemeindezugehoerigkeit)
1000 r.regierungsbezirk = self.rr.place.get_regierungsbezirk(ax.gemeindezugehoerigkeit)
1001 r.kreis = self.rr.place.get_kreis(ax.gemeindezugehoerigkeit)
1002 r.land = self.rr.place.get_land(ax.gemeindezugehoerigkeit)
1004 if self.rr.place.is_empty(r.gemarkung) or self.rr.place.is_empty(r.gemeinde):
1005 # exclude Flurstücke that refer to Gemeinde/Gemarkung
1006 # which do not exist in the reference AX tables
1007 self.counts['excluded'] += 1
1008 return None
1010 # geometry
1012 geom = _geom_of(r)
1013 if not geom:
1014 self.counts['no_geometry'] += 1
1015 return None
1017 r.geomFlaeche = round(geom.area, 2)
1018 r.x = round(geom.centroid.x, 2)
1019 r.y = round(geom.centroid.y, 2)
1021 self.counts['ok'] += 1
1022 return r
1024 def process_lage(self, fs: dt.Flurstueck):
1025 """Set the Lage list of a Flurstueck.
1027 Args:
1028 fs: Flurstueck.
1029 """
1031 fs.lageList = []
1033 # AX_Flurstueck.weistAuf -> AX_LagebezeichnungMitHausnummer
1034 # AX_Flurstueck.zeigtAuf -> AX_LagebezeichnungOhneHausnummer
1035 fs.lageList.extend(self.rr.lage.om.Lage.get_from_ptr(fs, '_weistAuf'))
1036 fs.lageList.extend(self.rr.lage.om.Lage.get_from_ptr(fs, '_zeigtAuf'))
1038 def process_gebaeude(self, fs: dt.Flurstueck):
1039 """Set the Gebaeude list and total building areas of a Flurstueck.
1041 Args:
1042 fs: Flurstueck, with its Lage list set.
1043 """
1045 ge_map = {}
1047 for la in fs.lageList:
1048 for ge in la.gebaeudeList:
1049 ge_map[ge.uid] = ge
1051 fs.gebaeudeList = list(ge_map.values())
1052 fs.gebaeudeList.sort(key=_sortkey_gebaeude)
1054 fs.gebaeudeAmtlicheFlaeche = sum(ge.recs[-1].amtlicheFlaeche for ge in fs.gebaeudeList if not ge.recs[-1].endet)
1055 fs.gebaeudeGeomFlaeche = sum(ge.recs[-1].geomFlaeche for ge in fs.gebaeudeList if not ge.recs[-1].endet)
1057 def process_buchung(self, fs: dt.Flurstueck):
1058 """Set the Buchung list of a Flurstueck.
1060 Collects the Buchungsstellen of all Flurstueck records, including their
1061 parents and children, and groups them by Buchungsblatt.
1063 Args:
1064 fs: Flurstueck.
1066 Returns:
1067 The Flurstueck.
1068 """
1070 bs_historic_map = {}
1071 bs_seen = set()
1072 buchung_map = {}
1074 # for each Flurstück record, we collect all related Buchungsstellen (with respect to parent-child relations)
1075 # then group Buchungsstellen by their Buchungsblatt
1076 # and create Buchung objects for a FS
1078 for r in fs.recs:
1079 hist_buchung = _pop(r, '_buchung')
1080 if hist_buchung:
1081 bs_list = self.historic_buchungsstelle_list(r, hist_buchung)
1082 else:
1083 bs_list = self.buchungsstelle_list(r)
1085 for bs in bs_list:
1086 # a Buchungsstelle referred to by an expired Flurstück might not be expired itself,
1087 # so we have to track its state separately by wrapping it in a BuchungsstelleReference
1088 # a BuchungsstelleReference is historic if its Flurstück is
1089 bs_historic_map[bs.uid] = r.isHistoric
1091 if bs.uid in bs_seen:
1092 continue
1093 bs_seen.add(bs.uid)
1095 # populate Flurstück references in a Buchungsstelle
1096 if fs.uid not in bs.fsUids:
1097 bs.fsUids.append(fs.uid)
1098 bs.flurstueckskennzeichenList.append(fs.flurstueckskennzeichen)
1100 # create Buchung records by grouping Buchungsstellen
1101 for bb_uid in bs.buchungsblattUids:
1102 bu = buchung_map.setdefault(bb_uid, dt.Buchung(recs=[], buchungsblattUid=bb_uid))
1103 bu.recs.append(dt.BuchungsstelleReference(buchungsstelle=bs))
1105 fs.buchungList = list(buchung_map.values())
1107 for bu in fs.buchungList:
1108 for ref in bu.recs:
1109 ref.isHistoric = bs_historic_map[ref.buchungsstelle.uid]
1110 bu.isHistoric = all(ref.isHistoric for ref in bu.recs)
1112 return fs
1114 def historic_buchungsstelle_list(self, r: dt.FlurstueckRecord, hist_buchung):
1115 """Create historic Buchungsstellen for a historic Flurstueck record.
1117 Args:
1118 r: Historic Flurstueck record.
1119 hist_buchung: ``buchung`` structs of the historic Flurstueck.
1121 Returns:
1122 A list of Buchungsstellen for the Buchungsblaetter that are found.
1123 """
1125 # an AX_HistorischesFlurstueck with a special 'buchung' reference
1127 bs_list = []
1129 for bu in hist_buchung:
1130 bb = self.rr.buchung.buchungsblattkennzeichenMap.get(bu.buchungsblattkennzeichen)
1131 if not bb:
1132 continue
1133 # create a fake historic Buchungstelle
1134 bs_list.append(
1135 dt.Buchungsstelle(
1136 uid=bb.uid + '_' + bu.laufendeNummerDerBuchungsstelle,
1137 recs=[
1138 dt.BuchungsstelleRecord(
1139 endet=r.endet,
1140 laufendeNummer=bu.laufendeNummerDerBuchungsstelle,
1141 isHistoric=True,
1142 )
1143 ],
1144 buchungsblattUids=[bb.uid],
1145 buchungsblattkennzeichenList=[bb.buchungsblattkennzeichen],
1146 parentUids=[],
1147 childUids=[],
1148 fsUids=[],
1149 parentkennzeichenList=[],
1150 flurstueckskennzeichenList=[],
1151 laufendeNummer=bu.laufendeNummerDerBuchungsstelle,
1152 isHistoric=True,
1153 )
1154 )
1156 return bs_list
1158 def buchungsstelle_list(self, r: dt.FlurstueckRecord):
1159 """Return the Buchungsstelle of a Flurstueck record with its parents and children.
1161 Args:
1162 r: Flurstueck record.
1164 Returns:
1165 A list of Buchungsstellen: the parents, the Buchungsstelle itself and the children.
1166 """
1168 # AX_Flurstueck.istGebucht -> AX_Buchungsstelle
1170 this_bs = self.rr.buchung.om.Buchungsstelle.get(_pop(r, '_istGebucht'))
1171 if not this_bs:
1172 return []
1174 bs_list = []
1176 # A Flurstück points to a Buchungsstelle (F.istGebucht -> B).
1177 # A Buchungsstelle can have parent (B.an -> parent.uid) and child (child.an -> B.uid) Buchungsstellen
1178 # (these references are populated in _BuchungIndexer above).
1179 # Our task here, given F.istGebucht -> B, collect B's parents and children
1180 # These are Buchungsstellen that directly or indirectly mention the current Flurstück.
1182 seen = {this_bs.uid}
1183 queue: list[dt.Buchungsstelle] = [this_bs]
1184 while queue:
1185 bs = queue.pop(0)
1186 bs_list.insert(0, bs)
1187 for parent_bs in self.rr.buchung.om.Buchungsstelle.get_many(bs.parentUids):
1188 if parent_bs.uid not in seen:
1189 seen.add(parent_bs.uid)
1190 queue.append(parent_bs)
1192 # remove this_bs
1193 bs_list.pop()
1195 seen = {this_bs.uid}
1196 queue: list[dt.Buchungsstelle] = [this_bs]
1197 while queue:
1198 bs = queue.pop(0)
1199 bs_list.append(bs)
1200 # sort related (child) Buchungsstellen by their BB-Kennzeichen
1201 child_bs_list = [c for c in self.rr.buchung.om.Buchungsstelle.get_many(bs.childUids) if c.uid not in seen]
1202 child_bs_list.sort(key=_sortkey_buchungsstelle_by_bblatt)
1203 seen.update(c.uid for c in child_bs_list)
1204 queue.extend(child_bs_list)
1206 # if len(bs_list) > 1:
1207 # gws.log.debug(f'bs chain: {r.uid=} {this_bs.uid=} {[bs.uid for bs in bs_list]}')
1209 return bs_list
1211 def write(self):
1212 values = []
1214 for fs in self.om.Flurstueck:
1215 geoms = [_pop(r, 'geom') for r in fs.recs]
1216 data = index.serialize(fs)
1217 for r, g in zip(fs.recs, geoms):
1218 r.geom = g
1220 values.append(
1221 dict(
1222 uid=fs.uid,
1223 rc=len(fs.recs),
1224 fshistoric=fs.isHistoric,
1225 data=data,
1226 geom=geoms[-1],
1227 )
1228 )
1230 self.write_table(index.TABLE_FLURSTUECK, values)
1233class _FsIndexIndexer(_Indexer):
1234 """Indexer for the flat search tables (``index*``)."""
1236 entries: dict[str, list[dict]]
1237 """Rows by table id."""
1239 def __init__(self, runner: '_Runner'):
1240 super().__init__(runner)
1241 self.entries = {
1242 index.TABLE_INDEXFLURSTUECK: [],
1243 index.TABLE_INDEXLAGE: [],
1244 index.TABLE_INDEXBUCHUNGSBLATT: [],
1245 index.TABLE_INDEXPERSON: [],
1246 index.TABLE_INDEXGEOM: [],
1247 }
1249 def collect(self):
1250 with ProgressIndicator(f'ALKIS: creating indexes', len(self.rr.fsdata.om.Flurstueck)) as progress:
1251 for fs in self.rr.fsdata.om.Flurstueck:
1252 for r in fs.recs:
1253 self.add(fs, r)
1254 progress.update(1)
1256 def add(self, fs: dt.Flurstueck, r: dt.FlurstueckRecord):
1257 """Add search table rows for a Flurstueck record.
1259 Args:
1260 fs: Flurstueck.
1261 r: Flurstueck record.
1262 """
1264 base = dict(
1265 fs=r.uid,
1266 fshistoric=r.isHistoric,
1267 )
1269 places = dict(
1270 land=r.land.text,
1271 land_t=index.text_key(r.land.text),
1272 landcode=r.land.code,
1273 regierungsbezirk=r.regierungsbezirk.text,
1274 regierungsbezirk_t=index.text_key(r.regierungsbezirk.text),
1275 regierungsbezirkcode=r.regierungsbezirk.code,
1276 kreis=r.kreis.text,
1277 kreis_t=index.text_key(r.kreis.text),
1278 kreiscode=r.kreis.code,
1279 gemeinde=r.gemeinde.text,
1280 gemeinde_t=index.text_key(r.gemeinde.text),
1281 gemeindecode=r.gemeinde.code,
1282 gemarkung=r.gemarkung.text,
1283 gemarkung_t=index.text_key(r.gemarkung.text),
1284 gemarkungcode=r.gemarkung.code,
1285 )
1287 self.entries[index.TABLE_INDEXFLURSTUECK].append(
1288 dict(
1289 **base,
1290 **places,
1291 amtlicheflaeche=r.amtlicheFlaeche,
1292 geomflaeche=r.geomFlaeche,
1293 flurnummer=r.flurnummer,
1294 zaehler=r.zaehler,
1295 nenner=r.nenner,
1296 flurstuecksfolge=r.flurstuecksfolge,
1297 flurstueckskennzeichen=r.flurstueckskennzeichen,
1298 x=r.x,
1299 y=r.y,
1300 )
1301 )
1303 self.entries[index.TABLE_INDEXGEOM].append(
1304 dict(
1305 **base,
1306 geomflaeche=r.geomFlaeche,
1307 x=r.x,
1308 y=r.y,
1309 geom=r.geom,
1310 )
1311 )
1313 for la in fs.lageList:
1314 for la_r in la.recs:
1315 self.entries[index.TABLE_INDEXLAGE].append(
1316 dict(
1317 **base,
1318 **places,
1319 lageuid=la_r.uid,
1320 lagehistoric=la_r.isHistoric,
1321 strasse=la_r.strasse,
1322 strasse_t=index.strasse_key(la_r.strasse),
1323 hausnummer=la_r.hausnummer,
1324 hausnummer_k=index.hausnummer_key(la_r.hausnummer),
1325 x=la.x or r.x,
1326 y=la.y or r.y,
1327 )
1328 )
1330 for bu in fs.buchungList:
1331 bb = self.rr.buchung.om.Buchungsblatt.get(bu.buchungsblattUid)
1332 if not bb:
1333 continue
1335 for bb_r in bb.recs:
1336 self.entries[index.TABLE_INDEXBUCHUNGSBLATT].append(
1337 dict(
1338 **base,
1339 buchungsblattuid=bb_r.uid,
1340 buchungsblattkennzeichen=bb_r.buchungsblattkennzeichen,
1341 buchungsblatthistoric=bu.isHistoric,
1342 )
1343 )
1345 pe_uids = set()
1347 for nn in bb.namensnummerList:
1348 for pe in nn.personList:
1349 if pe.uid in pe_uids:
1350 continue
1351 pe_uids.add(pe.uid)
1352 for pe_r in pe.recs:
1353 self.entries[index.TABLE_INDEXPERSON].append(
1354 dict(
1355 **base,
1356 personuid=pe_r.uid,
1357 personhistoric=pe_r.isHistoric,
1358 name=pe_r.nachnameOderFirma,
1359 name_t=index.text_key(pe_r.nachnameOderFirma),
1360 vorname=pe_r.vorname,
1361 vorname_t=index.text_key(pe_r.vorname),
1362 )
1363 )
1365 def write(self):
1366 for table_id, values in self.entries.items():
1367 if not self.ix.has_table(table_id):
1368 for n, v in enumerate(values, 1):
1369 v['n'] = n
1370 self.write_table(table_id, values)
1373class _Runner:
1374 """Runs all indexers in order and writes the index tables."""
1376 def __init__(self, ix: index.Object, reader: dt.Reader):
1377 """Create a runner.
1379 Args:
1380 ix: Index to build.
1381 reader: Source data reader.
1382 """
1384 self.ix: index.Object = ix
1385 self.reader: dt.Reader = reader
1387 self.place = _PlaceIndexer(self)
1388 self.lage = _LageIndexer(self)
1389 self.buchung = _BuchungIndexer(self)
1390 self.part = _PartIndexer(self)
1391 self.fsdata = _FsDataIndexer(self)
1392 self.fsindex = _FsIndexIndexer(self)
1394 self.initMemory = gws.lib.osx.process_rss_size()
1396 def run(self):
1397 """Collect all data and write the index tables."""
1399 with ProgressIndicator(f'ALKIS: indexing'):
1400 self.place.collect()
1401 self.memory_info()
1403 self.buchung.collect()
1404 self.memory_info()
1406 self.lage.collect()
1407 self.memory_info()
1409 self.fsdata.collect()
1410 gws.log.info(f'ALKIS: fs counts: {self.fsdata.counts}')
1411 self.memory_info()
1413 self.part.collect()
1414 self.memory_info()
1416 self.fsindex.collect()
1417 self.memory_info()
1419 self.place.write()
1420 self.buchung.write()
1421 self.lage.write()
1422 self.fsdata.write()
1423 self.part.write()
1424 self.fsindex.write()
1426 def memory_info(self):
1427 """Log the memory used since the runner was created."""
1429 v = gws.lib.osx.process_rss_size() - self.initMemory
1430 if v > 0:
1431 gws.log.info(f'ALKIS: memory used: {v:.2f} MB', stacklevel=2)
1433 def read_flat(self, cls):
1434 """Read all source objects of a type.
1436 Args:
1437 cls: GeoInfoDok class from ``gid``.
1439 Returns:
1440 A list of objects.
1441 """
1443 cnt = self.reader.count(cls)
1444 if cnt <= 0:
1445 gws.log.warning(f'ALKIS: read {cls.__name__}: empty table')
1446 return []
1448 rs = []
1449 with ProgressIndicator(f'ALKIS: read {cls.__name__}', cnt) as progress:
1450 for ax in self.reader.read_all(cls):
1451 rs.append(ax)
1452 progress.update(1)
1453 return rs
1455 def read_grouped(self, cls):
1456 """Read all source objects of a type, grouped by identifier.
1458 Args:
1459 cls: GeoInfoDok class from ``gid``.
1461 Returns:
1462 A list of ``(identifier, objects)`` tuples, objects sorted by start date.
1463 """
1465 cnt = self.reader.count(cls)
1466 if cnt <= 0:
1467 gws.log.warning(f'ALKIS: read {cls.__name__}: empty table')
1468 return []
1470 groups = {}
1471 with ProgressIndicator(f'ALKIS: read {cls.__name__}', cnt) as progress:
1472 for ax in self.reader.read_all(cls):
1473 groups.setdefault(ax.identifikator, []).append(ax)
1474 progress.update(1)
1475 for g in groups.values():
1476 g.sort(key=_sortkey_lebenszeitintervall)
1478 return list(groups.items())
1480 def props_from(self, ax, atts):
1481 """Extract descriptive properties from a source object.
1483 Object values and empty values are skipped, dates are formatted as ``DD.MM.YYYY``.
1485 Args:
1486 ax: Source object.
1487 atts: Attribute metadata.
1489 Returns:
1490 An object with the properties.
1491 """
1493 d = {}
1495 for a in atts:
1496 v = getattr(ax, a['name'], None)
1497 if isinstance(v, gid.Object):
1498 # @TODO handle properties which are objects
1499 continue
1500 if dtx.is_date(v):
1501 v = dtx.to_string('%d.%m.%Y', v)
1502 if not gws.u.is_empty(v):
1503 d[a['name']] = v
1505 return dt.Object(**d)
1508def _from_ax(cls, ax, **kwargs):
1509 """Create a record from a source object, its life span and extra values."""
1511 d = {}
1513 if ax:
1514 for k in cls.__annotations__:
1515 v = getattr(ax, k, None)
1516 if v:
1517 d[k] = v
1519 d['uid'] = ax.identifikator
1520 d['beginnt'] = ax.lebenszeitintervall.beginnt
1521 d['endet'] = ax.lebenszeitintervall.endet
1522 d['isHistoric'] = d['endet'] is not None
1523 if ax.anlass and ax.anlass[0].code != '000000':
1524 d['anlass'] = ax.anlass[0]
1525 if ax.geom:
1526 d['geom'] = ax.geom
1528 d.update(kwargs)
1529 return cls(**d)
1532def _anteil(ax):
1533 """Format the share of a source object as a fraction string."""
1535 try:
1536 z = float(ax.anteil.zaehler)
1537 z = str(int(z) if z.is_integer() else z)
1538 n = float(ax.anteil.nenner)
1539 n = str(int(n) if n.is_integer() else n)
1540 return z + '/' + n
1541 except (AttributeError, ValueError, TypeError):
1542 pass
1545def _meta_attributes(meta):
1546 """Return the attributes of a class, including inherited ones, that are known properties, sorted by title."""
1548 atts = {a['name']: a for a in _all_attributes(meta)}
1549 return sorted([a for a in atts.values() if a['name'] in dt.PROPS], key=lambda a: a['title'])
1552def _all_attributes(meta):
1553 """Yield the attributes of a class and its superclasses, superclasses first."""
1555 for sup in meta['supers']:
1556 if sup in gid.METADATA:
1557 yield from _all_attributes(gid.METADATA[sup])
1558 yield from meta['attributes']
1561def _geom_of(o):
1562 """Return the shapely geometry of an object, or ``None`` with a warning."""
1564 if not o.geom:
1565 gws.log.warning(f'{o.__class__.__name__}:{o.uid}: no geometry')
1566 return
1567 return shapely.wkb.loads(o.geom, hex=True)
1570def _pop(obj, attr):
1571 """Remove an attribute from an object and return its value."""
1573 v = getattr(obj, attr, None)
1574 try:
1575 delattr(obj, attr)
1576 except AttributeError:
1577 pass
1578 return v
1581def _sortkey_beginnt(o):
1582 return o.beginnt
1585def _sortkey_lebenszeitintervall(o):
1586 return o.lebenszeitintervall.beginnt
1589def _sortkey_namensnummer(nn: dt.Namensnummer):
1590 return _natkey(nn.recs[-1].laufendeNummerNachDIN1421), nn.recs[-1].beginnt
1593def _sortkey_buchungsstelle(bs: dt.Buchungsstelle):
1594 return _natkey(bs.recs[-1].laufendeNummer), bs.recs[-1].beginnt
1597def _sortkey_buchungsstelle_by_bblatt(bs: dt.Buchungsstelle):
1598 return bs.buchungsblattkennzeichenList[0], bs.recs[-1].beginnt
1601def _sortkey_part(pa: dt.Part):
1602 return pa.name.text, -pa.geomFlaeche
1605def _sortkey_gebaeude(ge: dt.Gebaeude):
1606 # sort Gebaeude by start date, then by area (big->small)
1607 return ge.recs[-1].beginnt, -ge.recs[-1].geomFlaeche
1610def _natkey(v):
1611 """Return a key for natural sorting of strings with numbers."""
1613 if not v:
1614 return []
1615 return ['{:080d}'.format(int(digits)) if digits else chars.lower() for digits, chars in re.findall(r'(\d+)|(\D+)', v.strip())]
1618def _comma(a):
1619 """Join values with commas, ``None`` becomes an empty string."""
1621 return ','.join(str(s) if s is not None else '' for s in a)
1624def _str(x):
1625 """Convert to a string, keeping ``None``."""
1627 return None if x is None else str(x)