Files
Plugin_SN_Verfahrensgebiet/functions/knickpunkt_pruefung.py
T

810 lines
28 KiB
Python
Raw Normal View History

"""sn_verfahrensgebiet/functions/knickpunkt_pruefung.py – Knickpunktbasierte BROR-Prüfung.
Vergleicht die Knickpunkte (Stützpunkte) des BauRaumOderBodenordnungsrecht-
Polygons mit den Punktorten der geladenen ``AX_PunktortTA``-Objekte und
identifiziert:
- **Identische Punkte** (grün): An einem BROR-Knickpunkt liegt ein PunktortTA.
- **Fehlende PunktortTA** (rot): Ein BROR-Knickpunkt hat keinen zugehörigen PunktortTA.
- **Fehlende Knickpunkte** (orange): Ein PunktortTA liegt innerhalb des
3-cm-Puffers einer Verbindungslinie zwischen zwei aufeinanderfolgenden
gematchten PunktortTA am BROR-Umring, hat aber keinen entsprechenden
Knickpunkt im BROR.
**Algorithmus:**
1. BROR-Polygon-Stützpunkte (Knickpunkte) extrahieren.
2. Spatial-Index auf PunktortTA aufbauen.
3. Für jeden Knickpunkt nächsten PunktortTA im Radius von
:data:`KOORDINATEN_TOLERANZ_M` suchen → grün oder rot.
Gematchte PunktortTA-Feature-IDs merken.
4. Entlang der BROR-Außengrenze aufeinanderfolgende gematchte PunktortTA
ermitteln und für jedes konsekutive Paar eine gerade Verbindungslinie
bilden; diese um :data:`VERBINDUNGSLINIE_PUFFER_M` puffern.
5. Nicht-gematchte PunktortTA innerhalb der Puffer → orange.
Verbindungslinien- und Puffer-Layer werden dem QGIS-Projekt hinzugefügt.
**Rückgabe:** ``(result_layer, identisch, kein_pta, fehlender_knick)``
"""
from __future__ import annotations
from typing import Dict, List, Optional, Set, Tuple
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorLayer,
QgsFeature,
QgsField,
QgsGeometry,
)
from sn_basis.functions.qt_wrapper import QVariant
# ---------------------------------------------------------------------------
# Konstanten
# ---------------------------------------------------------------------------
#: Koordinatentoleranz für die Knickpunkt ↔ PunktortTA-Übereinstimmung (m).
KOORDINATEN_TOLERANZ_M: float = 0.01
#: Puffer um den BROR-Umring für die Suche nach fehlenden Knickpunkten (m).
#: Wird aus Kompatibilitätsgründen behalten, wird aber nicht mehr verwendet.
BOUNDARY_PUFFER_M: float = 0.5
#: Puffer um die Verbindungslinie zwischen zwei aufeinanderfolgenden PunktortTA (m).
VERBINDUNGSLINIE_PUFFER_M: float = 0.03
#: Rundungsgenauigkeit für Vertex-Koordinaten im Flurstück-Set (Dezimalstellen).
_VERTEX_ROUND = 3
ERGEBNIS_LAYER_NAME = "Knickpunktprüfung"
FELD_TYP = "typ"
WERT_IDENTISCH = "identisch"
WERT_KEIN_PTA = "kein_pta"
WERT_FEHLENDER_KNICK = "fehlender_knick"
# ---------------------------------------------------------------------------
# Interne Hilfsfunktionen
# ---------------------------------------------------------------------------
def _extrahiere_knickpunkte(bror_layer) -> List[object]:
"""Extrahiert alle Stützpunkte (Knickpunkte) des BROR-Polygons.
Doppelte Start/End-Punkte eines Rings werden dedupliziert. Gibt eine
flache Liste von ``QgsPointXY``-Objekten zurück.
Parameters
----------
bror_layer :
BauRaumOderBodenordnungsrecht-Polygon-Layer.
Returns
-------
List[QgsPointXY]
Alle eindeutigen Stützpunkte; leer bei Fehler.
"""
try:
from qgis.core import QgsPointXY # type: ignore[import]
bror_feats = [
f for f in bror_layer.getFeatures()
if f.geometry() and not f.geometry().isEmpty()
]
if not bror_feats:
return []
union_geom = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
if union_geom is None or union_geom.isEmpty():
return []
punkte: List[object] = []
seen: Set[Tuple[float, float]] = set()
def _add_ring(vertices):
# Letzter Punkt = erster Punkt (Ring geschlossen) → überspringen
ring_pts = list(vertices)
n = len(ring_pts)
if n >= 2:
# Prüfen ob letzter Punkt identisch mit erstem
last = n - 1
if (
abs(ring_pts[last].x() - ring_pts[0].x()) < 1e-9
and abs(ring_pts[last].y() - ring_pts[0].y()) < 1e-9
):
ring_pts = ring_pts[:-1]
for pt in ring_pts:
key = (round(pt.x(), _VERTEX_ROUND), round(pt.y(), _VERTEX_ROUND))
if key not in seen:
seen.add(key)
punkte.append(pt)
geom_type = union_geom.wkbType()
# Polygon oder MultiPolygon
if union_geom.isMultipart():
for poly in union_geom.asMultiPolygon():
for ring in poly:
_add_ring([QgsPointXY(x, y) for x, y in ring])
else:
for ring in union_geom.asPolygon():
_add_ring([QgsPointXY(x, y) for x, y in ring])
return punkte
except Exception:
return []
def _erstelle_ergebnis_layer(crs_authid: str) -> object:
"""Erzeugt den leeren Punkt-Ergebnislayer mit Feld ``typ``.
Parameters
----------
crs_authid :
CRS-Kennung, z.B. ``"EPSG:25833"``.
Returns
-------
QgsVectorLayer
Leerer Memory-Layer vom Typ Point.
"""
layer = QgsVectorLayer(
f"Point?crs={crs_authid}", ERGEBNIS_LAYER_NAME, "memory"
)
pr = layer.dataProvider()
pr.addAttributes([QgsField(FELD_TYP, QVariant.String)])
layer.updateFields()
return layer
def _setze_kategorien_symbologie(layer) -> None:
"""Wendet Kategoriesymbologie an: grün/rot/orange Punkte.
- ``identisch`` → grün
- ``kein_pta`` → rot
- ``fehlender_knick`` → orange
Wird bei fehlendem QGIS-Kontext still ignoriert.
"""
try:
from qgis.core import ( # type: ignore[import]
QgsCategorizedSymbolRenderer,
QgsRendererCategory,
QgsMarkerSymbol,
)
def _marker(color: str, size: str = "2") -> object:
return QgsMarkerSymbol.createSimple(
{"color": color, "size": size, "outline_style": "no"}
)
categories = [
QgsRendererCategory(
WERT_IDENTISCH,
_marker("0,180,0,255"),
"identisch (Knickpunkt mit PunktortTA)",
),
QgsRendererCategory(
WERT_KEIN_PTA,
_marker("220,0,0,255"),
"kein PunktortTA an Knickpunkt",
),
QgsRendererCategory(
WERT_FEHLENDER_KNICK,
_marker("255,128,0,255"),
"fehlender Knickpunkt (PunktortTA ohne BROR-Knickpunkt)",
),
]
renderer = QgsCategorizedSymbolRenderer(FELD_TYP, categories)
layer.setRenderer(renderer)
except Exception:
pass # Kein QGIS-Kontext (Tests) → Standarddarstellung behalten
def _baue_pta_index(punktort_layer) -> Tuple[object, Dict[int, object]]:
"""Baut einen QgsSpatialIndex und eine ID→Geometrie-Map für PunktortTA.
Parameters
----------
punktort_layer :
Geladener ``AX_PunktortTA``-Layer.
Returns
-------
Tuple[QgsSpatialIndex, Dict[int, QgsGeometry]]
Index und Feature-ID-zu-Geometrie-Dict.
"""
from qgis.core import QgsSpatialIndex # type: ignore[import]
index = QgsSpatialIndex()
geom_map: Dict[int, object] = {}
for feat in punktort_layer.getFeatures():
geom = feat.geometry()
if geom and not geom.isEmpty():
index.insertFeature(feat)
geom_map[feat.id()] = geom
return index, geom_map
def _fehlende_knickpunkte_via_verbindungslinien(
bror_union,
pta_index,
geom_map: Dict[int, object],
matched_pta_ids: Set[int],
punktort_layer,
crs_authid: str,
) -> Tuple[int, List[object]]:
"""Ermittelt fehlende Knickpunkte via Verbindungslinien-Puffer.
Bestimmt entlang der BROR-Außengrenze die Reihenfolge der gematchten
``AX_PunktortTA``. Für jedes konsekutive Paar wird eine gerade
Verbindungslinie erzeugt und um :data:`VERBINDUNGSLINIE_PUFFER_M` gepuffert.
Nicht-gematchte PunktortTA innerhalb dieses Puffers werden als fehlende
Knickpunkte (orange) gewertet.
Fügt dem QGIS-Projekt zwei Hilfs-Layer hinzu (falls ein Projekt-Kontext
vorhanden ist):
- *Knickpunktprüfung – Verbindungslinien*
- *Knickpunktprüfung – Puffer*
Parameters
----------
bror_union :
Vereinigungsgeometrie des BROR-Polygons (``QgsGeometry``).
pta_index :
Spatial-Index der PunktortTA-Features (``QgsSpatialIndex``).
geom_map :
Feature-ID → ``QgsGeometry``-Mapping für PunktortTA.
matched_pta_ids :
IDs der bereits gematchten PunktortTA (identisch mit BROR-Knickpunkten).
punktort_layer :
Geladener ``AX_PunktortTA``-Layer (für zukünftige Erweiterungen
reserviert; wird derzeit nicht direkt verwendet).
crs_authid :
CRS-Kennung des BROR-Layers, z.B. ``"EPSG:25833"``.
Returns
-------
Tuple[int, List[QgsFeature]]
``(fehlender_knick_count, orange_feats)`` – Anzahl gefundener fehlender
Knickpunkte und Liste der zugehörigen ``QgsFeature``-Objekte.
"""
try:
from qgis.core import QgsPointXY, QgsProject # type: ignore[import]
except ImportError:
return 0, []
if bror_union is None or bror_union.isEmpty():
return 0, []
# ------------------------------------------------------------------
2026-07-10 09:29:25 +02:00
# Alle Ringe extrahieren (Außen- und Innenringe)
# ------------------------------------------------------------------
2026-07-10 09:29:25 +02:00
all_rings: List[List[object]] = []
if bror_union.isMultipart():
for poly in bror_union.asMultiPolygon():
2026-07-10 09:29:25 +02:00
for ring in poly:
all_rings.append([QgsPointXY(x, y) for x, y in ring])
else:
2026-07-10 09:29:25 +02:00
for ring in bror_union.asPolygon():
all_rings.append([QgsPointXY(x, y) for x, y in ring])
# Schlusspunkt (= Startpunkt, Ring geschlossen) entfernen
cleaned_rings: List[List[object]] = []
2026-07-10 09:29:25 +02:00
for ring in all_rings:
if len(ring) >= 2:
if (
abs(ring[-1].x() - ring[0].x()) < 1e-9
and abs(ring[-1].y() - ring[0].y()) < 1e-9
):
ring = ring[:-1]
if len(ring) >= 2:
cleaned_rings.append(ring)
line_geoms: List[object] = []
buffer_geoms: List[object] = []
orange_ids: Set[int] = set()
# ------------------------------------------------------------------
# Pro Ring: gematchte PunktortTA in Reihenfolge, Paare bilden
# ------------------------------------------------------------------
for ring_pts in cleaned_rings:
# Gematchte PunktortTA auf diesem Ring lokalisieren
pta_on_ring: List[Tuple[int, int, object]] = [] # (ring_idx, fid, pt)
for fid in matched_pta_ids:
pta_geom = geom_map.get(fid)
if pta_geom is None:
continue
pta_pt = pta_geom.asPoint()
for i, ring_pt in enumerate(ring_pts):
dist_sq = (
(ring_pt.x() - pta_pt.x()) ** 2
+ (ring_pt.y() - pta_pt.y()) ** 2
)
if dist_sq <= KOORDINATEN_TOLERANZ_M ** 2:
pta_on_ring.append((i, fid, ring_pt))
break
if len(pta_on_ring) < 2:
continue
pta_on_ring.sort(key=lambda t: t[0])
num_pta = len(pta_on_ring)
for k in range(num_pta):
_, _, pt1 = pta_on_ring[k]
_, _, pt2 = pta_on_ring[(k + 1) % num_pta]
# Gerade Verbindungslinie zwischen den zwei PunktortTA
line_geom = QgsGeometry.fromPolylineXY([pt1, pt2])
if line_geom is None or line_geom.isEmpty():
continue
line_geoms.append(line_geom)
buffer_geom = line_geom.buffer(VERBINDUNGSLINIE_PUFFER_M, 8)
if buffer_geom is None or buffer_geom.isEmpty():
continue
buffer_geoms.append(buffer_geom)
# Nicht-gematchte PunktortTA im Puffer suchen (via Spatial-Index)
bb = buffer_geom.boundingBox()
kandidaten = pta_index.intersects(bb)
for fid in kandidaten:
if fid in matched_pta_ids or fid in orange_ids:
continue
candidate_geom = geom_map.get(fid)
if candidate_geom is None:
continue
try:
if candidate_geom.within(buffer_geom):
orange_ids.add(fid)
except Exception:
continue
# ------------------------------------------------------------------
# Orange Features erstellen
# ------------------------------------------------------------------
orange_feats: List[object] = []
for fid in orange_ids:
pta_geom = geom_map.get(fid)
if pta_geom is None:
continue
feat = QgsFeature()
feat.setGeometry(pta_geom)
feat.setAttributes([WERT_FEHLENDER_KNICK])
orange_feats.append(feat)
# ------------------------------------------------------------------
# Hilfs-Layer erstellen und zum Projekt hinzufügen
# ------------------------------------------------------------------
try:
lines_layer = QgsVectorLayer(
f"LineString?crs={crs_authid}",
"Knickpunktprüfung – Verbindungslinien",
"memory",
)
lines_pr = lines_layer.dataProvider()
line_feats_list = []
for lg in line_geoms:
f = QgsFeature()
f.setGeometry(lg)
line_feats_list.append(f)
lines_pr.addFeatures(line_feats_list)
lines_layer.updateExtents()
buffer_layer = QgsVectorLayer(
f"Polygon?crs={crs_authid}",
"Knickpunktprüfung – Puffer",
"memory",
)
buffer_pr = buffer_layer.dataProvider()
buffer_feats_list = []
for bg in buffer_geoms:
f = QgsFeature()
f.setGeometry(bg)
buffer_feats_list.append(f)
buffer_pr.addFeatures(buffer_feats_list)
buffer_layer.updateExtents()
project = QgsProject.instance()
if project is not None:
project.addMapLayer(lines_layer)
project.addMapLayer(buffer_layer)
except Exception:
pass # Kein QGIS-Kontext (Tests) → Layer-Erstellung überspringen
return len(orange_feats), orange_feats
# ---------------------------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------------------------
def knickpunkt_pruefung(
bror_layer,
punktort_layer,
flst_layer,
progress=None,
knickpunkte: Optional[List] = None,
) -> Tuple[Optional[object], int, int, int]:
"""Knickpunktbasierte BROR-Prüfung.
Vergleicht BROR-Knickpunkte mit ``AX_PunktortTA``-Objekten und ermittelt
fehlende Knickpunkte anhand von Flurstücksgrenzen.
Parameters
----------
bror_layer :
BauRaumOderBodenordnungsrecht-Polygon-Layer.
punktort_layer :
Geladener ``AX_PunktortTA``-Layer (Ergebnis aus ``grenzpunkt_alkis``).
flst_layer :
Nicht mehr verwendet. Wird aus Kompatibilitätsgründen weiterhin
akzeptiert.
progress :
Optionaler ``ProgressDialog`` für Fortschrittsanzeige.
knickpunkte :
Vorberechnete Knickpunktliste (optional). Wenn ``None``, werden die
Knickpunkte aus ``bror_layer`` extrahiert.
Returns
-------
Tuple[Optional[QgsVectorLayer], int, int, int]
``(result_layer, identisch_count, kein_pta_count, fehlender_knick_count)``
- ``result_layer``: Punkt-Layer mit grünen/roten/orangen Punkten,
oder ``None`` bei Abbruch oder fehlgeschlagener Verarbeitung.
- ``identisch_count``: Knickpunkte mit passendem PunktortTA (grün).
- ``kein_pta_count``: Knickpunkte ohne passenden PunktortTA (rot).
- ``fehlender_knick_count``: PunktortTA ohne BROR-Knickpunkt (orange).
"""
def _canceled() -> bool:
return progress is not None and progress.is_canceled()
def _schritt(label: str, wert: int) -> None:
if progress is not None:
progress.set_label(label)
progress.set_value(wert)
# ------------------------------------------------------------------
# Schritt 0: Knickpunkte ermitteln
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: Knickpunkte extrahieren …", 0)
if knickpunkte is None:
knickpunkte = _extrahiere_knickpunkte(bror_layer)
if not knickpunkte:
return None, 0, 0, 0
n = len(knickpunkte)
crs_authid = bror_layer.crs().authid()
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritt 1: Spatial-Index für PunktortTA aufbauen
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: PunktortTA-Index aufbauen …", 1)
if punktort_layer is None:
return None, 0, 0, 0
try:
pta_index, geom_map = _baue_pta_index(punktort_layer)
except Exception:
return None, 0, 0, 0
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritte 2…n+1: Jeden Knickpunkt mit PunktortTA vergleichen
# ------------------------------------------------------------------
try:
from qgis.core import QgsRectangle # type: ignore[import]
except ImportError:
return None, 0, 0, 0
ergebnis_layer = _erstelle_ergebnis_layer(crs_authid)
pr = ergebnis_layer.dataProvider()
feats_to_add: List[object] = []
identisch_count = 0
kein_pta_count = 0
matched_pta_ids: Set[int] = set()
for i, knick_pt in enumerate(knickpunkte):
if _canceled():
return None, 0, 0, 0
_schritt(f"Knickpunktprüfung: Knickpunkt {i + 1}/{n} prüfen …", i + 2)
x, y = knick_pt.x(), knick_pt.y()
t = KOORDINATEN_TOLERANZ_M
suchbox = QgsRectangle(x - t, y - t, x + t, y + t)
kandidaten = pta_index.intersects(suchbox)
gefunden = False
for fid in kandidaten:
pta_geom = geom_map.get(fid)
if pta_geom is None:
continue
pta_pt = pta_geom.asPoint()
dist = ((pta_pt.x() - x) ** 2 + (pta_pt.y() - y) ** 2) ** 0.5
if dist <= KOORDINATEN_TOLERANZ_M:
gefunden = True
matched_pta_ids.add(fid)
break
feat = QgsFeature()
try:
from qgis.core import QgsPointXY # type: ignore[import]
feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
except Exception:
continue
feat.setAttributes([WERT_IDENTISCH if gefunden else WERT_KEIN_PTA])
feats_to_add.append(feat)
if gefunden:
identisch_count += 1
else:
kein_pta_count += 1
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritt n+2: Fehlende Knickpunkte (orange) via Verbindungslinien
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: Fehlende Knickpunkte ermitteln …", n + 2)
fehlender_knick_count = 0
try:
bror_feats_union = [
f for f in bror_layer.getFeatures()
if f.geometry() and not f.geometry().isEmpty()
]
bror_union_geom = QgsGeometry.unaryUnion(
[f.geometry() for f in bror_feats_union]
)
except Exception:
bror_union_geom = None
if not _canceled():
fehlender_knick_count, orange_feats = _fehlende_knickpunkte_via_verbindungslinien(
bror_union_geom,
pta_index,
geom_map,
matched_pta_ids,
punktort_layer,
crs_authid,
)
feats_to_add.extend(orange_feats)
if _canceled():
return None, 0, 0, 0
# ------------------------------------------------------------------
# Schritt n+3: Ergebnislayer fertigstellen
# ------------------------------------------------------------------
_schritt("Knickpunktprüfung: Ergebnislayer erstellen …", n + 3)
pr.addFeatures(feats_to_add)
ergebnis_layer.updateExtents()
_setze_kategorien_symbologie(ergebnis_layer)
return ergebnis_layer, identisch_count, kein_pta_count, fehlender_knick_count
def erstelle_korrigiertes_polygon(
bror_layer,
result_layer,
crs_authid: str,
) -> Tuple[Optional[object], Optional[object]]:
"""Erstellt korrigierte Verbindungslinien und ein Polygon aus identischen
und gefundenen fehlenden Knickpunkten.
Kombiniert die gematchten PunktortTA (``identisch``, grün) mit den
gefundenen fehlenden Knickpunkten (``fehlender_knick``, orange) zu einem
korrigierten Verlauf entlang der BROR-Außengrenze. Orange Punkte werden
nach ihrer Projektion auf die Verbindungsstrecke zwischen ihren zwei
umgebenden identischen PunktortTA einsortiert.
Parameters
----------
bror_layer :
BauRaumOderBodenordnungsrecht-Polygon-Layer (für die Ringreihenfolge).
result_layer :
Punkt-Ergebnislayer aus :func:`knickpunkt_pruefung` mit Feld ``typ``.
crs_authid :
CRS-Kennung, z.B. ``"EPSG:25833"``.
Returns
-------
Tuple[Optional[QgsVectorLayer], Optional[QgsVectorLayer]]
``(linien_layer, polygon_layer)`` – Memory-Layer mit den korrigierten
Verbindungslinien und dem abgeleiteten Polygon, oder ``(None, None)``
bei fehlgeschlagener Verarbeitung.
"""
try:
from qgis.core import QgsPointXY # type: ignore[import]
except ImportError:
return None, None
# ------------------------------------------------------------------
# Punkte aus Ergebnislayer extrahieren
# ------------------------------------------------------------------
identisch_pts: List[object] = []
orange_pts: List[object] = []
try:
for feat in result_layer.getFeatures():
geom = feat.geometry()
if geom is None or geom.isEmpty():
continue
pt = geom.asPoint()
typ = feat.attribute(FELD_TYP)
if typ == WERT_IDENTISCH:
identisch_pts.append(QgsPointXY(pt.x(), pt.y()))
elif typ == WERT_FEHLENDER_KNICK:
orange_pts.append(QgsPointXY(pt.x(), pt.y()))
except Exception:
return None, None
if len(identisch_pts) < 3:
return None, None
# ------------------------------------------------------------------
2026-07-10 09:29:25 +02:00
# Alle Polygon-Teile mit allen Ringen (Außen- und Innenringe)
# ------------------------------------------------------------------
try:
bror_feats = [
f for f in bror_layer.getFeatures()
if f.geometry() and not f.geometry().isEmpty()
]
bror_union = QgsGeometry.unaryUnion([f.geometry() for f in bror_feats])
if bror_union is None or bror_union.isEmpty():
return None, None
if bror_union.isMultipart():
2026-07-10 09:29:25 +02:00
all_poly_parts = [poly for poly in bror_union.asMultiPolygon() if poly]
else:
poly = bror_union.asPolygon()
2026-07-10 09:29:25 +02:00
all_poly_parts = [poly] if poly else []
except Exception:
return None, None
2026-07-10 09:29:25 +02:00
if not all_poly_parts:
return None, None
all_line_segments: List[Tuple[object, object]] = []
all_polygon_feats: List[object] = []
# ------------------------------------------------------------------
2026-07-10 09:29:25 +02:00
# Pro Polygon-Teil: alle Ringe (Außen + Innen) verarbeiten
# ------------------------------------------------------------------
2026-07-10 09:29:25 +02:00
for poly_rings in all_poly_parts:
corrected_qgs_rings: List[List[object]] = []
2026-07-10 09:29:25 +02:00
for raw_ring in poly_rings:
ring_pts = [QgsPointXY(x, y) for x, y in raw_ring]
if len(ring_pts) >= 2 and (
abs(ring_pts[-1].x() - ring_pts[0].x()) < 1e-9
and abs(ring_pts[-1].y() - ring_pts[0].y()) < 1e-9
):
ring_pts = ring_pts[:-1]
2026-07-10 09:29:25 +02:00
if len(ring_pts) < 3:
continue
2026-07-10 09:29:25 +02:00
# Identisch-Punkte auf diesem Ring lokalisieren
on_ring: List[Tuple[int, object]] = []
for pt in identisch_pts:
for i, rpt in enumerate(ring_pts):
dist_sq = (rpt.x() - pt.x()) ** 2 + (rpt.y() - pt.y()) ** 2
if dist_sq <= KOORDINATEN_TOLERANZ_M ** 2:
on_ring.append((i, pt))
break
2026-07-10 09:29:25 +02:00
on_ring.sort(key=lambda t: t[0])
2026-07-10 09:29:25 +02:00
if len(on_ring) < 3:
# Fallback: Original-Ring verwenden
ordered_pts: List[object] = ring_pts
for j in range(len(ring_pts)):
all_line_segments.append(
2026-07-10 09:29:25 +02:00
(ring_pts[j], ring_pts[(j + 1) % len(ring_pts)])
)
2026-07-10 09:29:25 +02:00
else:
# Korrigierten Verlauf für diesen Ring aufbauen
ordered_pts = []
n_part = len(on_ring)
2026-07-10 09:29:25 +02:00
for k in range(n_part):
_, pt1 = on_ring[k]
_, pt2 = on_ring[(k + 1) % n_part]
dx = pt2.x() - pt1.x()
dy = pt2.y() - pt1.y()
len_sq = dx * dx + dy * dy
between: List[Tuple[float, object]] = []
if len_sq > 0:
for opt in orange_pts:
t = (
(opt.x() - pt1.x()) * dx + (opt.y() - pt1.y()) * dy
) / len_sq
clamped_t = max(0.0, min(1.0, t))
foot_x = pt1.x() + clamped_t * dx
foot_y = pt1.y() + clamped_t * dy
dist = (
(opt.x() - foot_x) ** 2 + (opt.y() - foot_y) ** 2
) ** 0.5
if dist <= VERBINDUNGSLINIE_PUFFER_M:
between.append((t, opt))
between.sort(key=lambda x: x[0])
segment_pts = [pt1] + [p for _, p in between]
ordered_pts.extend(segment_pts)
all_pts_in_seg = segment_pts + [pt2]
for j in range(len(all_pts_in_seg) - 1):
all_line_segments.append(
(all_pts_in_seg[j], all_pts_in_seg[j + 1])
)
if len(ordered_pts) >= 3:
corrected_qgs_rings.append(ordered_pts + [ordered_pts[0]])
if not corrected_qgs_rings:
continue
2026-07-10 09:29:25 +02:00
# Erstes Element = Außenring, weitere = Innenringe (Löcher)
polygon_geom = QgsGeometry.fromPolygonXY(corrected_qgs_rings)
if polygon_geom and not polygon_geom.isEmpty():
pf = QgsFeature()
pf.setGeometry(polygon_geom)
all_polygon_feats.append(pf)
if not all_polygon_feats:
return None, None
# ------------------------------------------------------------------
# Linien-Layer erstellen
# ------------------------------------------------------------------
linien_layer = QgsVectorLayer(
f"LineString?crs={crs_authid}",
"Korrigierte Verbindungslinien (Detailprüfung)",
"memory",
)
lines_pr = linien_layer.dataProvider()
line_feats_list = []
for p1, p2 in all_line_segments:
lgeom = QgsGeometry.fromPolylineXY([p1, p2])
if lgeom and not lgeom.isEmpty():
f = QgsFeature()
f.setGeometry(lgeom)
line_feats_list.append(f)
lines_pr.addFeatures(line_feats_list)
linien_layer.updateExtents()
# ------------------------------------------------------------------
# Polygon-Layer erstellen (ein Feature pro Teilgebiet)
# ------------------------------------------------------------------
polygon_layer = QgsVectorLayer(
f"Polygon?crs={crs_authid}",
"Korrigiertes Polygon (Detailprüfung)",
"memory",
)
poly_pr = polygon_layer.dataProvider()
poly_pr.addFeatures(all_polygon_feats)
polygon_layer.updateExtents()
return linien_layer, polygon_layer