810 lines
28 KiB
Python
810 lines
28 KiB
Python
"""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, []
|
||
|
||
# ------------------------------------------------------------------
|
||
# Alle Ringe extrahieren (Außen- und Innenringe)
|
||
# ------------------------------------------------------------------
|
||
all_rings: List[List[object]] = []
|
||
if bror_union.isMultipart():
|
||
for poly in bror_union.asMultiPolygon():
|
||
for ring in poly:
|
||
all_rings.append([QgsPointXY(x, y) for x, y in ring])
|
||
else:
|
||
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]] = []
|
||
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
|
||
|
||
# ------------------------------------------------------------------
|
||
# 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():
|
||
all_poly_parts = [poly for poly in bror_union.asMultiPolygon() if poly]
|
||
else:
|
||
poly = bror_union.asPolygon()
|
||
all_poly_parts = [poly] if poly else []
|
||
except Exception:
|
||
return None, None
|
||
|
||
if not all_poly_parts:
|
||
return None, None
|
||
|
||
all_line_segments: List[Tuple[object, object]] = []
|
||
all_polygon_feats: List[object] = []
|
||
|
||
# ------------------------------------------------------------------
|
||
# Pro Polygon-Teil: alle Ringe (Außen + Innen) verarbeiten
|
||
# ------------------------------------------------------------------
|
||
for poly_rings in all_poly_parts:
|
||
corrected_qgs_rings: List[List[object]] = []
|
||
|
||
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]
|
||
|
||
if len(ring_pts) < 3:
|
||
continue
|
||
|
||
# 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
|
||
|
||
on_ring.sort(key=lambda t: t[0])
|
||
|
||
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(
|
||
(ring_pts[j], ring_pts[(j + 1) % len(ring_pts)])
|
||
)
|
||
else:
|
||
# Korrigierten Verlauf für diesen Ring aufbauen
|
||
ordered_pts = []
|
||
n_part = len(on_ring)
|
||
|
||
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
|
||
|
||
# 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
|