2026-05-05 19:46:08 +02:00
|
|
|
|
"""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.
|
2026-07-10 07:33:31 +02:00
|
|
|
|
- **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.
|
2026-05-05 19:46:08 +02:00
|
|
|
|
|
|
|
|
|
|
**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.
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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.
|
2026-05-05 19:46:08 +02:00
|
|
|
|
|
|
|
|
|
|
**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).
|
2026-07-10 07:33:31 +02:00
|
|
|
|
#: Wird aus Kompatibilitätsgründen behalten, wird aber nicht mehr verwendet.
|
2026-05-05 19:46:08 +02:00
|
|
|
|
BOUNDARY_PUFFER_M: float = 0.5
|
|
|
|
|
|
|
2026-07-10 07:33:31 +02:00
|
|
|
|
#: Puffer um die Verbindungslinie zwischen zwei aufeinanderfolgenden PunktortTA (m).
|
|
|
|
|
|
VERBINDUNGSLINIE_PUFFER_M: float = 0.03
|
|
|
|
|
|
|
2026-05-05 19:46:08 +02:00
|
|
|
|
#: 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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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*
|
2026-05-05 19:46:08 +02:00
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
----------
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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"``.
|
2026-05-05 19:46:08 +02:00
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
-------
|
2026-07-10 07:33:31 +02:00
|
|
|
|
Tuple[int, List[QgsFeature]]
|
|
|
|
|
|
``(fehlender_knick_count, orange_feats)`` – Anzahl gefundener fehlender
|
|
|
|
|
|
Knickpunkte und Liste der zugehörigen ``QgsFeature``-Objekte.
|
2026-05-05 19:46:08 +02:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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 07:33:31 +02:00
|
|
|
|
# ------------------------------------------------------------------
|
2026-07-10 09:29:25 +02:00
|
|
|
|
all_rings: List[List[object]] = []
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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])
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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])
|
2026-07-10 07:33:31 +02:00
|
|
|
|
|
|
|
|
|
|
# Schlusspunkt (= Startpunkt, Ring geschlossen) entfernen
|
|
|
|
|
|
cleaned_rings: List[List[object]] = []
|
2026-07-10 09:29:25 +02:00
|
|
|
|
for ring in all_rings:
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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:
|
2026-05-05 19:46:08 +02:00
|
|
|
|
continue
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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():
|
2026-05-05 19:46:08 +02:00
|
|
|
|
continue
|
2026-07-10 07:33:31 +02:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-05 19:46:08 +02:00
|
|
|
|
except Exception:
|
2026-07-10 07:33:31 +02:00
|
|
|
|
pass # Kein QGIS-Kontext (Tests) → Layer-Erstellung überspringen
|
|
|
|
|
|
|
|
|
|
|
|
return len(orange_feats), orange_feats
|
2026-05-05 19:46:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Ö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 :
|
2026-07-10 07:33:31 +02:00
|
|
|
|
Nicht mehr verwendet. Wird aus Kompatibilitätsgründen weiterhin
|
|
|
|
|
|
akzeptiert.
|
2026-05-05 19:46:08 +02:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
2026-07-10 07:33:31 +02:00
|
|
|
|
# Schritt n+2: Fehlende Knickpunkte (orange) via Verbindungslinien
|
2026-05-05 19:46:08 +02:00
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
_schritt("Knickpunktprüfung: Fehlende Knickpunkte ermitteln …", n + 2)
|
|
|
|
|
|
|
|
|
|
|
|
fehlender_knick_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
2026-07-10 07:33:31 +02:00
|
|
|
|
bror_feats_union = [
|
2026-05-05 19:46:08 +02:00
|
|
|
|
f for f in bror_layer.getFeatures()
|
|
|
|
|
|
if f.geometry() and not f.geometry().isEmpty()
|
|
|
|
|
|
]
|
2026-07-10 07:33:31 +02:00
|
|
|
|
bror_union_geom = QgsGeometry.unaryUnion(
|
|
|
|
|
|
[f.geometry() for f in bror_feats_union]
|
2026-05-05 19:46:08 +02:00
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
2026-07-10 07:33:31 +02:00
|
|
|
|
bror_union_geom = None
|
2026-05-05 19:46:08 +02:00
|
|
|
|
|
2026-07-10 07:33:31 +02:00
|
|
|
|
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)
|
2026-05-05 19:46:08 +02:00
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-10 08:24:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-07-10 08:24:10 +02:00
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
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]
|
2026-07-10 08:24:10 +02:00
|
|
|
|
else:
|
|
|
|
|
|
poly = bror_union.asPolygon()
|
2026-07-10 09:29:25 +02:00
|
|
|
|
all_poly_parts = [poly] if poly else []
|
2026-07-10 08:24:10 +02:00
|
|
|
|
except Exception:
|
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
2026-07-10 09:29:25 +02:00
|
|
|
|
if not all_poly_parts:
|
2026-07-10 08:24:10 +02:00
|
|
|
|
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 08:24:10 +02:00
|
|
|
|
# ------------------------------------------------------------------
|
2026-07-10 09:29:25 +02:00
|
|
|
|
for poly_rings in all_poly_parts:
|
|
|
|
|
|
corrected_qgs_rings: List[List[object]] = []
|
2026-07-10 08:24:10 +02:00
|
|
|
|
|
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 08:24:10 +02:00
|
|
|
|
|
2026-07-10 09:29:25 +02:00
|
|
|
|
if len(ring_pts) < 3:
|
|
|
|
|
|
continue
|
2026-07-10 08:24:10 +02:00
|
|
|
|
|
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 08:24:10 +02:00
|
|
|
|
|
2026-07-10 09:29:25 +02:00
|
|
|
|
on_ring.sort(key=lambda t: t[0])
|
2026-07-10 08:24:10 +02:00
|
|
|
|
|
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)):
|
2026-07-10 08:24:10 +02:00
|
|
|
|
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 08:24:10 +02:00
|
|
|
|
)
|
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 08:24:10 +02:00
|
|
|
|
|
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:
|
2026-07-10 08:24:10 +02:00
|
|
|
|
continue
|
|
|
|
|
|
|
2026-07-10 09:29:25 +02:00
|
|
|
|
# Erstes Element = Außenring, weitere = Innenringe (Löcher)
|
|
|
|
|
|
polygon_geom = QgsGeometry.fromPolygonXY(corrected_qgs_rings)
|
2026-07-10 08:24:10 +02:00
|
|
|
|
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
|