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

1"""Build the ALKIS index from source data.""" 

2 

3from typing import Optional, Generic, TypeVar 

4from collections.abc import Iterator 

5 

6import re 

7 

8import shapely 

9import shapely.strtree 

10import shapely.wkb 

11 

12import gws 

13import gws.lib.osx 

14import gws.lib.datetimex as dtx 

15from gws.lib.cli import ProgressIndicator 

16 

17from . import types as dt 

18from . import index 

19from . import norbit 

20from . import gid 

21 

22 

23def run(ix: index.Object, data_schema: str, with_force=False): 

24 """Build the ALKIS index. 

25 

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. 

29 

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. 

34 

35 Raises: 

36 ``gws.Error``: If the index schema does not exist. 

37 """ 

38 

39 if not ix.has_schema(): 

40 raise gws.Error(f'ALKIS: schema {ix.schema!r} does not exist') 

41 

42 if with_force: 

43 ix.drop() 

44 elif ix.status().complete: 

45 return 

46 

47 rdr = norbit.Object(ix.db, schema=data_schema) 

48 rr = _Runner(ix, rdr) 

49 rr.run() 

50 

51 

52## 

53 

54T = TypeVar('T') 

55 

56 

57class _ObjectDict(Generic[T]): 

58 """Collection of entities of one type, by uid.""" 

59 

60 def __init__(self, cls): 

61 """Create an empty collection. 

62 

63 Args: 

64 cls: Entity class. 

65 """ 

66 

67 self.d = {} 

68 self.cls = cls 

69 

70 def add(self, uid, recs) -> T: 

71 """Create an entity and add it to the collection. 

72 

73 The entity is historic if all of its records are. 

74 

75 Args: 

76 uid: Entity uid. 

77 recs: Entity records. 

78 

79 Returns: 

80 The new entity. 

81 """ 

82 

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 

87 

88 def get(self, uid, default=None) -> Optional[T]: 

89 """Return an entity by uid. 

90 

91 Args: 

92 uid: Entity uid. 

93 default: Value to return if the uid is not found. 

94 

95 Returns: 

96 The entity or the default value. 

97 """ 

98 

99 return self.d.get(uid, default) 

100 

101 def get_many(self, uids) -> list[T]: 

102 """Return entities by uids. 

103 

104 Unknown uids are skipped, duplicates are returned once. 

105 

106 Args: 

107 uids: Entity uids. 

108 

109 Returns: 

110 A list of entities, in the order of ``uids``. 

111 """ 

112 

113 res = {} 

114 

115 for uid in uids: 

116 if uid not in res: 

117 o = self.d.get(uid) 

118 if o: 

119 res[uid] = o 

120 

121 return list(res.values()) 

122 

123 def get_from_ptr(self, obj: dt.Entity, attr): 

124 """Return the entities referenced by an attribute of the records of an entity. 

125 

126 The attribute is removed from the records. 

127 

128 Args: 

129 obj: Entity whose records hold the references. 

130 attr: Attribute name, holding a uid or a list of uids. 

131 

132 Returns: 

133 A list of referenced entities. 

134 """ 

135 

136 uids = [] 

137 

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) 

144 

145 return self.get_many(uids) 

146 

147 def __iter__(self) -> Iterator[T]: 

148 yield from self.d.values() 

149 

150 def __len__(self): 

151 return len(self.d) 

152 

153 

154class _ObjectMap: 

155 """Collections of all entities collected by an indexer.""" 

156 

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) 

167 

168 self.placeAll: dict = {} 

169 self.placeIdx: dict = {} 

170 self.catalog: dict = {} 

171 

172 

173class _Indexer: 

174 """Base class for indexers. 

175 

176 An indexer collects entities of some kinds from the source data into its 

177 object map and writes them into index tables. 

178 """ 

179 

180 def __init__(self, runner: '_Runner'): 

181 """Create an indexer. 

182 

183 Args: 

184 runner: Runner that owns this indexer. 

185 """ 

186 

187 self.rr = runner 

188 self.ix: index.Object = runner.ix 

189 self.om = _ObjectMap() 

190 

191 def collect(self): 

192 """Collect entities from the source data into the object map.""" 

193 

194 pass 

195 

196 def write_table(self, table_id, values): 

197 """Create and fill an index table, unless it already has data. 

198 

199 Args: 

200 table_id: Table id. 

201 values: Rows as dicts. 

202 """ 

203 

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) 

208 

209 def write(self): 

210 """Write the collected entities into the index tables.""" 

211 

212 pass 

213 

214 

215class _PlaceIndexer(_Indexer): 

216 """Indexer for places (Administrations- und Verwaltungseinheiten). 

217 

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 """ 

223 

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.""" 

228 

229 def add(self, kind, ax, key_obj, **kwargs): 

230 """Add a place, unless it is historic. 

231 

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. 

237 

238 Returns: 

239 The place value, or ``None`` if the place is historic. 

240 """ 

241 

242 if ax.lebenszeitintervall.endet is not None: 

243 return 

244 

245 code = self.code(kind, key_obj) 

246 value = dt.EnumPair(code, ax.bezeichnung) 

247 

248 p = dt.Place(**kwargs) 

249 

250 p.uid = kind + code 

251 p.kind = kind 

252 setattr(p, kind, value) 

253 

254 self.om.placeAll[p.uid] = p 

255 self.om.placeIdx[p.uid] = value 

256 

257 return value 

258 

259 def collect(self): 

260 self.om.placeAll = {} 

261 self.om.placeIdx = {} 

262 

263 for ax in self.rr.read_flat(gid.AX_Bundesland): 

264 self.add('land', ax, ax.schluessel) 

265 

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)) 

269 

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)) 

273 

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)) 

277 

278 # @TODO map Gemarkung to Gemeinde (see https://de.wikipedia.org/wiki/Liste_der_Gemarkungen_in_Nordrhein-Westfalen etc) 

279 

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)) 

285 

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)) 

289 

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)) 

293 

294 def write(self): 

295 values = [] 

296 

297 for place in self.om.placeAll.values(): 

298 values.append( 

299 dict( 

300 uid=place.uid, 

301 data=index.serialize(place), 

302 ) 

303 ) 

304 

305 self.write_table(index.TABLE_PLACE, values) 

306 

307 def get_land(self, o): 

308 """Return the Land for a key object. 

309 

310 Args: 

311 o: Key object. 

312 

313 Returns: 

314 The place value, or an empty value if not found. 

315 """ 

316 

317 return self.get('land', o) or self.empty2 

318 

319 def get_regierungsbezirk(self, o): 

320 """Return the Regierungsbezirk for a key object. 

321 

322 Args: 

323 o: Key object. 

324 

325 Returns: 

326 The place value, or an empty value if not found. 

327 """ 

328 

329 return self.get('regierungsbezirk', o) or self.empty1 

330 

331 def get_kreis(self, o): 

332 """Return the Kreis for a key object. 

333 

334 Args: 

335 o: Key object. 

336 

337 Returns: 

338 The place value, or an empty value if not found. 

339 """ 

340 

341 return self.get('kreis', o) or self.empty2 

342 

343 def get_gemeinde(self, o): 

344 """Return the Gemeinde for a key object. 

345 

346 Args: 

347 o: Key object. 

348 

349 Returns: 

350 The place value, or an empty value if not found. 

351 """ 

352 

353 return self.get('gemeinde', o) or self.empty1 

354 

355 def get_gemarkung(self, o): 

356 """Return the Gemarkung for a key object. 

357 

358 Args: 

359 o: Key object. 

360 

361 Returns: 

362 The place value, or an empty value if not found. 

363 """ 

364 

365 return self.get('gemarkung', o) or self.empty1 

366 

367 def get_buchungsblattbezirk(self, o): 

368 """Return the Buchungsblattbezirk for a key object. 

369 

370 Args: 

371 o: Key object. 

372 

373 Returns: 

374 The place value, or an empty value if not found. 

375 """ 

376 

377 return self.get('buchungsblattbezirk', o) or self.empty1 

378 

379 def get_dienststelle(self, o): 

380 """Return the Dienststelle for a key object. 

381 

382 Args: 

383 o: Key object. 

384 

385 Returns: 

386 The place value, or an empty value if not found. 

387 """ 

388 

389 return self.get('dienststelle', o) or self.empty1 

390 

391 def get(self, kind, o): 

392 """Return a place by kind and key object. 

393 

394 Args: 

395 kind: Place kind. 

396 o: Key object. 

397 

398 Returns: 

399 The place value, or ``None`` if not found. 

400 """ 

401 

402 return self.om.placeIdx.get(kind + self.code(kind, o)) 

403 

404 def is_empty(self, p: dt.EnumPair): 

405 """Check whether a place value is empty. 

406 

407 Args: 

408 p: Place value. 

409 

410 Returns: 

411 ``True`` if the code is ``0`` or ``00``. 

412 """ 

413 

414 return p.code == '0' or p.code == '00' 

415 

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.""" 

426 

427 def code(self, kind, o): 

428 """Build a place code. 

429 

430 Args: 

431 kind: Place kind. 

432 o: Key object. 

433 

434 Returns: 

435 The place code. 

436 """ 

437 

438 return self.CODES[kind](o) 

439 

440 

441class _LageIndexer(_Indexer): 

442 """Indexer for location designations (Lage) and buildings (Gebaeude). 

443 

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 """ 

448 

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 

452 

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 ) 

467 

468 # use the PTO (art=HNR) geometry for lage coordinates 

469 # PTO.dientZurDarstellungVon -> lage.uid 

470 

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 

477 

478 uids = _pop(pto, 'dientZurDarstellungVon') 

479 if not uids or not isinstance(uids, list): 

480 continue 

481 

482 geom = _geom_of(pto) 

483 if not geom: 

484 continue 

485 

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 

491 

492 # read related Gebaeude records 

493 

494 atts = _meta_attributes(gid.METADATA['AX_Gebaeude']) 

495 

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 ) 

511 

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 

516 

517 # omit Gebaeude geometries for now 

518 for ge in self.om.Gebaeude: 

519 for r in ge.recs: 

520 _pop(r, 'geom') 

521 

522 for la in self.om.Lage: 

523 la.gebaeudeList = [] 

524 

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) 

529 

530 def strasse(self, ax): 

531 """Return the street name of a source Lage object. 

532 

533 Args: 

534 ax: Source Lage object. 

535 

536 Returns: 

537 The unencoded street name, or the name from the catalog for an encoded one. 

538 """ 

539 

540 if isinstance(ax.lagebezeichnung, str): 

541 return ax.lagebezeichnung 

542 return self.om.catalog.get(self.lage_key(ax.lagebezeichnung), '') 

543 

544 def lage_key(self, r): 

545 """Return the catalog key for an encoded location. 

546 

547 Args: 

548 r: Encoded location or catalog key object. 

549 

550 Returns: 

551 A string of the key parts joined with commas. 

552 """ 

553 

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 ) 

563 

564 def write(self): 

565 values = [] 

566 

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 ) 

575 

576 self.write_table(index.TABLE_LAGE, values) 

577 

578 

579class _BuchungIndexer(_Indexer): 

580 """Indexer for land register data. 

581 

582 Collects Buchungsblaetter with their Buchungsstellen and Namensnummern, 

583 Persons and their Anschriften, and the parent-child relations of 

584 Buchungsstellen. 

585 """ 

586 

587 buchungsblattkennzeichenMap: dict[str, dt.Buchungsblatt] 

588 """Buchungsblaetter by their identifier.""" 

589 

590 def __init__(self, runner: '_Runner'): 

591 super().__init__(runner) 

592 self.buchungsblattkennzeichenMap = {} 

593 

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 ) 

609 

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 ) 

623 

624 # AX_Person.hat -> [AX_Anschrift] 

625 for pe in self.om.Person: 

626 pe.anschriftList = self.om.Anschrift.get_from_ptr(pe, '_hat') 

627 

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 ) 

633 

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') 

638 

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 ) 

654 

655 for bs in self.om.Buchungsstelle: 

656 bs.laufendeNummer = bs.recs[-1].laufendeNummer 

657 bs.fsUids = [] 

658 bs.flurstueckskennzeichenList = [] 

659 

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 ) 

672 

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 

678 

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) 

686 

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) 

694 

695 for bb in self.om.Buchungsblatt: 

696 bb.buchungsstelleList.sort(key=_sortkey_buchungsstelle) 

697 bb.namensnummerList.sort(key=_sortkey_namensnummer) 

698 

699 # AX_Buchungsstelle.an -> [AX_Buchungsstelle] 

700 # AX_Buchungsstelle.zu -> [AX_Buchungsstelle] 

701 # see Erläuterungen zu ALKIS Version 6, page 116-119 

702 

703 for bs in self.om.Buchungsstelle: 

704 bs.childUids = [] 

705 bs.parentUids = [] 

706 bs.parentkennzeichenList = [] 

707 

708 for bs in self.om.Buchungsstelle: 

709 parent_uids = set() 

710 parent_knz = set() 

711 

712 for r in bs.recs: 

713 parent_uids.update(_pop(r, '_an')) 

714 parent_uids.update(_pop(r, '_zu')) 

715 

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) 

721 

722 bs.parentkennzeichenList = sorted(parent_knz) 

723 

724 def write(self): 

725 values = [] 

726 

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 ) 

735 

736 self.write_table(index.TABLE_BUCHUNGSBLATT, values) 

737 

738 

739class _PartIndexer(_Indexer): 

740 """Indexer for Parts (Nutzung, Festlegung, Bewertung). 

741 

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 """ 

746 

747 MIN_PART_AREA = 1 

748 """Minimum area of an intersection.""" 

749 

750 parts: list[dt.Part] 

751 """Computed parts.""" 

752 

753 fs_list: list[dt.Flurstueck] 

754 """Flurstuecke, in the order of ``fs_geom``.""" 

755 fs_geom: list 

756 """Most recent Flurstueck geometries.""" 

757 

758 stree: shapely.strtree.STRtree 

759 """Spatial index of ``fs_geom``.""" 

760 

761 def __init__(self, runner: '_Runner'): 

762 super().__init__(runner) 

763 self.parts = [] 

764 self.fs_list = [] 

765 self.fs_geom = [] 

766 

767 def collect(self): 

768 

769 for kind in dt.Part.KIND: 

770 self.collect_kind(kind) 

771 

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])) 

776 

777 self.stree = shapely.strtree.STRtree(self.fs_geom) 

778 

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) 

783 

784 self.parts.sort(key=_sortkey_part) 

785 

786 for pa in self.parts: 

787 pa.isHistoric = all(r.isHistoric for r in pa.recs) 

788 

789 def collect_kind(self, kind): 

790 """Collect source objects of all object types of a Part kind. 

791 

792 Args: 

793 kind: Part kind. 

794 """ 

795 

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 ] 

800 

801 for cls in classes: 

802 self.collect_class(kind, cls) 

803 

804 def collect_class(self, kind, cls): 

805 """Collect source objects of one object type. 

806 

807 Args: 

808 kind: Part kind. 

809 cls: GeoInfoDok class from ``gid``. 

810 """ 

811 

812 meta = gid.METADATA[cls.__name__] 

813 atts = _meta_attributes(meta) 

814 

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']) 

829 

830 def compute_intersections(self, pa: dt.Part): 

831 """Intersect a source object with the Flurstuecke and add the resulting parts. 

832 

833 Args: 

834 pa: Source object as a Part. 

835 """ 

836 

837 parts_map = {} 

838 

839 for r in pa.recs: 

840 geom = _geom_of(r) 

841 if not geom: 

842 continue 

843 

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 

849 

850 fs = self.fs_list[i] 

851 

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 ) 

862 

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) 

865 

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 ) 

878 

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 

882 

883 self.parts.extend(parts_map.values()) 

884 

885 def write(self): 

886 values = [] 

887 

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 ) 

906 

907 self.write_table(index.TABLE_PART, values) 

908 

909 

910class _FsDataIndexer(_Indexer): 

911 """Indexer for Flurstuecke. 

912 

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 """ 

918 

919 def __init__(self, runner: '_Runner'): 

920 super().__init__(runner) 

921 self.counts = { 

922 'ok': 0, 

923 'no_geometry': 0, 

924 'excluded': 0, 

925 } 

926 

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) 

932 

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) 

944 

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) 

953 

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 

957 

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) 

971 

972 def record(self, ax): 

973 """Create a Flurstueck record from a source object. 

974 

975 Args: 

976 ax: Source Flurstueck object. 

977 

978 Returns: 

979 The record, or ``None`` if the Flurstueck has no geometry or an unknown Gemarkung or Gemeinde. 

980 """ 

981 

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 ) 

995 

996 # basic data 

997 

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) 

1003 

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 

1009 

1010 # geometry 

1011 

1012 geom = _geom_of(r) 

1013 if not geom: 

1014 self.counts['no_geometry'] += 1 

1015 return None 

1016 

1017 r.geomFlaeche = round(geom.area, 2) 

1018 r.x = round(geom.centroid.x, 2) 

1019 r.y = round(geom.centroid.y, 2) 

1020 

1021 self.counts['ok'] += 1 

1022 return r 

1023 

1024 def process_lage(self, fs: dt.Flurstueck): 

1025 """Set the Lage list of a Flurstueck. 

1026 

1027 Args: 

1028 fs: Flurstueck. 

1029 """ 

1030 

1031 fs.lageList = [] 

1032 

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')) 

1037 

1038 def process_gebaeude(self, fs: dt.Flurstueck): 

1039 """Set the Gebaeude list and total building areas of a Flurstueck. 

1040 

1041 Args: 

1042 fs: Flurstueck, with its Lage list set. 

1043 """ 

1044 

1045 ge_map = {} 

1046 

1047 for la in fs.lageList: 

1048 for ge in la.gebaeudeList: 

1049 ge_map[ge.uid] = ge 

1050 

1051 fs.gebaeudeList = list(ge_map.values()) 

1052 fs.gebaeudeList.sort(key=_sortkey_gebaeude) 

1053 

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) 

1056 

1057 def process_buchung(self, fs: dt.Flurstueck): 

1058 """Set the Buchung list of a Flurstueck. 

1059 

1060 Collects the Buchungsstellen of all Flurstueck records, including their 

1061 parents and children, and groups them by Buchungsblatt. 

1062 

1063 Args: 

1064 fs: Flurstueck. 

1065 

1066 Returns: 

1067 The Flurstueck. 

1068 """ 

1069 

1070 bs_historic_map = {} 

1071 bs_seen = set() 

1072 buchung_map = {} 

1073 

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 

1077 

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) 

1084 

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 

1090 

1091 if bs.uid in bs_seen: 

1092 continue 

1093 bs_seen.add(bs.uid) 

1094 

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) 

1099 

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)) 

1104 

1105 fs.buchungList = list(buchung_map.values()) 

1106 

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) 

1111 

1112 return fs 

1113 

1114 def historic_buchungsstelle_list(self, r: dt.FlurstueckRecord, hist_buchung): 

1115 """Create historic Buchungsstellen for a historic Flurstueck record. 

1116 

1117 Args: 

1118 r: Historic Flurstueck record. 

1119 hist_buchung: ``buchung`` structs of the historic Flurstueck. 

1120 

1121 Returns: 

1122 A list of Buchungsstellen for the Buchungsblaetter that are found. 

1123 """ 

1124 

1125 # an AX_HistorischesFlurstueck with a special 'buchung' reference 

1126 

1127 bs_list = [] 

1128 

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 ) 

1155 

1156 return bs_list 

1157 

1158 def buchungsstelle_list(self, r: dt.FlurstueckRecord): 

1159 """Return the Buchungsstelle of a Flurstueck record with its parents and children. 

1160 

1161 Args: 

1162 r: Flurstueck record. 

1163 

1164 Returns: 

1165 A list of Buchungsstellen: the parents, the Buchungsstelle itself and the children. 

1166 """ 

1167 

1168 # AX_Flurstueck.istGebucht -> AX_Buchungsstelle 

1169 

1170 this_bs = self.rr.buchung.om.Buchungsstelle.get(_pop(r, '_istGebucht')) 

1171 if not this_bs: 

1172 return [] 

1173 

1174 bs_list = [] 

1175 

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. 

1181 

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) 

1191 

1192 # remove this_bs 

1193 bs_list.pop() 

1194 

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) 

1205 

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]}') 

1208 

1209 return bs_list 

1210 

1211 def write(self): 

1212 values = [] 

1213 

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 

1219 

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 ) 

1229 

1230 self.write_table(index.TABLE_FLURSTUECK, values) 

1231 

1232 

1233class _FsIndexIndexer(_Indexer): 

1234 """Indexer for the flat search tables (``index*``).""" 

1235 

1236 entries: dict[str, list[dict]] 

1237 """Rows by table id.""" 

1238 

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 } 

1248 

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) 

1255 

1256 def add(self, fs: dt.Flurstueck, r: dt.FlurstueckRecord): 

1257 """Add search table rows for a Flurstueck record. 

1258 

1259 Args: 

1260 fs: Flurstueck. 

1261 r: Flurstueck record. 

1262 """ 

1263 

1264 base = dict( 

1265 fs=r.uid, 

1266 fshistoric=r.isHistoric, 

1267 ) 

1268 

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 ) 

1286 

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 ) 

1302 

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 ) 

1312 

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 ) 

1329 

1330 for bu in fs.buchungList: 

1331 bb = self.rr.buchung.om.Buchungsblatt.get(bu.buchungsblattUid) 

1332 if not bb: 

1333 continue 

1334 

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 ) 

1344 

1345 pe_uids = set() 

1346 

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 ) 

1364 

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) 

1371 

1372 

1373class _Runner: 

1374 """Runs all indexers in order and writes the index tables.""" 

1375 

1376 def __init__(self, ix: index.Object, reader: dt.Reader): 

1377 """Create a runner. 

1378 

1379 Args: 

1380 ix: Index to build. 

1381 reader: Source data reader. 

1382 """ 

1383 

1384 self.ix: index.Object = ix 

1385 self.reader: dt.Reader = reader 

1386 

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) 

1393 

1394 self.initMemory = gws.lib.osx.process_rss_size() 

1395 

1396 def run(self): 

1397 """Collect all data and write the index tables.""" 

1398 

1399 with ProgressIndicator(f'ALKIS: indexing'): 

1400 self.place.collect() 

1401 self.memory_info() 

1402 

1403 self.buchung.collect() 

1404 self.memory_info() 

1405 

1406 self.lage.collect() 

1407 self.memory_info() 

1408 

1409 self.fsdata.collect() 

1410 gws.log.info(f'ALKIS: fs counts: {self.fsdata.counts}') 

1411 self.memory_info() 

1412 

1413 self.part.collect() 

1414 self.memory_info() 

1415 

1416 self.fsindex.collect() 

1417 self.memory_info() 

1418 

1419 self.place.write() 

1420 self.buchung.write() 

1421 self.lage.write() 

1422 self.fsdata.write() 

1423 self.part.write() 

1424 self.fsindex.write() 

1425 

1426 def memory_info(self): 

1427 """Log the memory used since the runner was created.""" 

1428 

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) 

1432 

1433 def read_flat(self, cls): 

1434 """Read all source objects of a type. 

1435 

1436 Args: 

1437 cls: GeoInfoDok class from ``gid``. 

1438 

1439 Returns: 

1440 A list of objects. 

1441 """ 

1442 

1443 cnt = self.reader.count(cls) 

1444 if cnt <= 0: 

1445 gws.log.warning(f'ALKIS: read {cls.__name__}: empty table') 

1446 return [] 

1447 

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 

1454 

1455 def read_grouped(self, cls): 

1456 """Read all source objects of a type, grouped by identifier. 

1457 

1458 Args: 

1459 cls: GeoInfoDok class from ``gid``. 

1460 

1461 Returns: 

1462 A list of ``(identifier, objects)`` tuples, objects sorted by start date. 

1463 """ 

1464 

1465 cnt = self.reader.count(cls) 

1466 if cnt <= 0: 

1467 gws.log.warning(f'ALKIS: read {cls.__name__}: empty table') 

1468 return [] 

1469 

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) 

1477 

1478 return list(groups.items()) 

1479 

1480 def props_from(self, ax, atts): 

1481 """Extract descriptive properties from a source object. 

1482 

1483 Object values and empty values are skipped, dates are formatted as ``DD.MM.YYYY``. 

1484 

1485 Args: 

1486 ax: Source object. 

1487 atts: Attribute metadata. 

1488 

1489 Returns: 

1490 An object with the properties. 

1491 """ 

1492 

1493 d = {} 

1494 

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 

1504 

1505 return dt.Object(**d) 

1506 

1507 

1508def _from_ax(cls, ax, **kwargs): 

1509 """Create a record from a source object, its life span and extra values.""" 

1510 

1511 d = {} 

1512 

1513 if ax: 

1514 for k in cls.__annotations__: 

1515 v = getattr(ax, k, None) 

1516 if v: 

1517 d[k] = v 

1518 

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 

1527 

1528 d.update(kwargs) 

1529 return cls(**d) 

1530 

1531 

1532def _anteil(ax): 

1533 """Format the share of a source object as a fraction string.""" 

1534 

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 

1543 

1544 

1545def _meta_attributes(meta): 

1546 """Return the attributes of a class, including inherited ones, that are known properties, sorted by title.""" 

1547 

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']) 

1550 

1551 

1552def _all_attributes(meta): 

1553 """Yield the attributes of a class and its superclasses, superclasses first.""" 

1554 

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'] 

1559 

1560 

1561def _geom_of(o): 

1562 """Return the shapely geometry of an object, or ``None`` with a warning.""" 

1563 

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) 

1568 

1569 

1570def _pop(obj, attr): 

1571 """Remove an attribute from an object and return its value.""" 

1572 

1573 v = getattr(obj, attr, None) 

1574 try: 

1575 delattr(obj, attr) 

1576 except AttributeError: 

1577 pass 

1578 return v 

1579 

1580 

1581def _sortkey_beginnt(o): 

1582 return o.beginnt 

1583 

1584 

1585def _sortkey_lebenszeitintervall(o): 

1586 return o.lebenszeitintervall.beginnt 

1587 

1588 

1589def _sortkey_namensnummer(nn: dt.Namensnummer): 

1590 return _natkey(nn.recs[-1].laufendeNummerNachDIN1421), nn.recs[-1].beginnt 

1591 

1592 

1593def _sortkey_buchungsstelle(bs: dt.Buchungsstelle): 

1594 return _natkey(bs.recs[-1].laufendeNummer), bs.recs[-1].beginnt 

1595 

1596 

1597def _sortkey_buchungsstelle_by_bblatt(bs: dt.Buchungsstelle): 

1598 return bs.buchungsblattkennzeichenList[0], bs.recs[-1].beginnt 

1599 

1600 

1601def _sortkey_part(pa: dt.Part): 

1602 return pa.name.text, -pa.geomFlaeche 

1603 

1604 

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 

1608 

1609 

1610def _natkey(v): 

1611 """Return a key for natural sorting of strings with numbers.""" 

1612 

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())] 

1616 

1617 

1618def _comma(a): 

1619 """Join values with commas, ``None`` becomes an empty string.""" 

1620 

1621 return ','.join(str(s) if s is not None else '' for s in a) 

1622 

1623 

1624def _str(x): 

1625 """Convert to a string, keeping ``None``.""" 

1626 

1627 return None if x is None else str(x)