"""Eine von Hand ergänzte Straße ins Netz einfügen, mit echten Knoten an ihren Kreuzungen.""" from __future__ import annotations from leonida.roads import Edge, Graph, Node, insert_edge def _kanten_enden_auf_ihrem_knoten(graph: Graph) -> None: """Invariante über den ganzen Graphen: jede Kante muss geometrisch auf den Knoten treffen, an dem sie laut a/b hängt - sonst baut section.py ihre Kreuzungsarme am falschen Punkt.""" for edge in graph.edges: assert edge.points[0] == graph.nodes[edge.a].point assert edge.points[-1] == graph.nodes[edge.b].point def kreuz() -> Graph: """Eine 100 m lange Straße bei y = 50, von (0,50) nach (100,50).""" return Graph(nodes=[Node((0.0, 50.0)), Node((100.0, 50.0))], edges=[Edge(0, 1, [(0.0, 50.0), (100.0, 50.0)], 12.0, "wohnstrasse")]) def test_kreuzung_in_der_mitte_bekommt_einen_knoten_mit_grad_4(): out = insert_edge(kreuz(), [(50.0, 0.0), (50.0, 100.0)], 12.0, "wohnstrasse") knoten = next(k for k, n in enumerate(out.nodes) if n.point == (50.0, 50.0)) assert out.degree(knoten) == 4 assert not any(e.a == e.b for e in out.edges) _kanten_enden_auf_ihrem_knoten(out) def t_kreuzung() -> Graph: """Ein T bei (50,0): die Strecke (0,0)-(100,0) durchgehend, ein Ast nach Norden - Grad 3.""" return Graph(nodes=[Node((0.0, 0.0)), Node((100.0, 0.0)), Node((50.0, 0.0)), Node((50.0, 50.0))], edges=[Edge(0, 2, [(0.0, 0.0), (50.0, 0.0)], 12.0, "wohnstrasse"), Edge(2, 1, [(50.0, 0.0), (100.0, 0.0)], 12.0, "wohnstrasse"), Edge(2, 3, [(50.0, 0.0), (50.0, 50.0)], 12.0, "wohnstrasse")]) def test_neues_ende_haengt_sich_an_einen_knoten_mit_grad_3(): graph = t_kreuzung() assert graph.degree(2) == 3 # (54,6) liegt 7,2 m von Knoten 2 entfernt (< JUNCTION_RADIUS), aber auf keiner der drei # vorhandenen Kanten - reine Anhänge-Nähe, keine Kreuzung. out = insert_edge(graph, [(54.0, 6.0), (94.0, 46.0)], 6.0, "gasse") assert out.degree(2) == 4 assert len(out.nodes) == 5, "nur das ferne Ende der neuen Gasse ist ein neuer Knoten" _kanten_enden_auf_ihrem_knoten(out) def kreuz_mit_nahem_knoten() -> Graph: """kreuz(), dazu ein Sackgassenfuss 5,7 m neben der kommenden Kreuzung bei (50,50).""" return Graph(nodes=[Node((0.0, 50.0)), Node((100.0, 50.0)), Node((54.0, 54.0)), Node((54.0, 90.0))], edges=[Edge(0, 1, [(0.0, 50.0), (100.0, 50.0)], 12.0, "wohnstrasse"), Edge(2, 3, [(54.0, 54.0), (54.0, 90.0)], 6.0, "gasse")]) def test_kreuzungspunkt_nahe_einem_knoten_bekommt_keinen_zweiten(): """Regel b: ein Kreuzungspunkt im 8-m-Umkreis eines vorhandenen Knotens bekommt keinen eigenen neuen Knoten - sonst lägen zwei Knoten nur wenige Meter auseinander.""" out = insert_edge(kreuz_mit_nahem_knoten(), [(50.0, 0.0), (50.0, 100.0)], 12.0, "wohnstrasse") assert not any(n.point == (50.0, 50.0) for n in out.nodes), "kein zweiter Knoten 5,7 m daneben" assert len(out.nodes) == 6 # 4 vorhandene + 2 neue Sackgassen # Am wiederverwendeten Knoten (54,54) treffen sich jetzt der alte Sackgassenfuss (Grad 1) und # die neue Kreuzung selbst (zwei gekreuzte Straßen, je zweimal gespalten: Grad 4) - macht 5. assert out.degree(2) == 5 _kanten_enden_auf_ihrem_knoten(out) def test_kanten_enden_immer_auf_ihrem_knoten(): """Regressionswache (Fixrunde 2, Befund 1): attach() darf einen bis zu JUNCTION_RADIUS entfernten Knoten wiederverwenden, aber dann muss die Kante zu ihm hinknicken. Mit der alten Fassung endete hier die Kreuzungskante bei (50,50), obwohl sie an Knoten 2 - tatsächlich bei (54,54) - hängt: 5,66 m Lücke, genau der Fehler, den section.py in junction_arms() nicht verzeiht, weil dort mit edge.points gerechnet wird, nicht mit dem Knotenpunkt.""" out = insert_edge(kreuz_mit_nahem_knoten(), [(50.0, 0.0), (50.0, 100.0)], 12.0, "wohnstrasse") _kanten_enden_auf_ihrem_knoten(out) def bestandskante() -> Graph: return Graph(nodes=[Node((0.0, 0.0)), Node((200.0, 0.0))], edges=[Edge(0, 1, [(0.0, 0.0), (200.0, 0.0)], 12.0, "wohnstrasse")]) def test_kreuzung_nach_ueberlappung_geht_nicht_verloren(): """Regressionswache (Fixrunde 2, Befund 2): läuft die neue Linie ein Stück lang deckungsgleich mit einer Bestandskante und kreuzt sie danach an einer anderen Stelle, liefert intersection() eine GeometryCollection aus der Überlappung (LineString) und dem echten Kreuzungspunkt (Point) - _crossing_points() darf dabei nicht die ganze Fläche verwerfen, nur weil sie kein reiner Point/MultiPoint ist, sonst bleibt die Bestandskante an einer echten Kreuzung ungeteilt. """ linie = [(0.0, 0.0), (50.0, 0.0), (50.0, -50.0), (150.0, -50.0), (150.0, 0.0), (150.0, 50.0)] out = insert_edge(bestandskante(), linie, 6.0, "gasse") knoten = next(k for k, n in enumerate(out.nodes) if n.point == (150.0, 0.0)) assert out.degree(knoten) >= 3 _kanten_enden_auf_ihrem_knoten(out) def zwei_kreuzungen_nah_beieinander() -> Graph: """Zwei parallele Kanten 5 m auseinander (unter JUNCTION_RADIUS) - beim Netzaufbau aus dem Skelett gäbe es dort ohnehin nur einen Kreuzungsknoten, nie zwei so nah beieinander.""" return Graph(nodes=[Node((0.0, 50.0)), Node((100.0, 50.0)), Node((0.0, 55.0)), Node((100.0, 55.0))], edges=[Edge(0, 1, [(0.0, 50.0), (100.0, 50.0)], 12.0, "wohnstrasse"), Edge(2, 3, [(0.0, 55.0), (100.0, 55.0)], 12.0, "wohnstrasse")]) def test_kreuzungen_naeher_als_der_radius_werden_ein_knoten(): """Festhalten, nicht ändern (Fixrunde 2, Punkt 3): Kreuzt die neue Linie zwei verschiedene Kanten weniger als JUNCTION_RADIUS auseinander, verschmelzen beide zu einem Knoten - dieselbe Regel wie beim Aufbau des Graphen aus dem Skelett (_junction_clusters()): Alles innerhalb des Radius ist eine einzige Kreuzung, nicht zwei dicht nebeneinander. """ out = insert_edge(zwei_kreuzungen_nah_beieinander(), [(50.0, 0.0), (50.0, 100.0)], 12.0, "wohnstrasse") berührt = {e.a for e in out.edges} | {e.b for e in out.edges} kreuzungen = [k for k in berührt if out.degree(k) >= 3] assert len(kreuzungen) == 1, "die beiden Kreuzungspunkte müssen auf denselben Knoten fallen" # Beide Bestandskanten tragen je 2 bei (ihr eigener Zweiteil-Schnitt); von der neuen Linie # bleiben nur die beiden äußeren Stücke (Nord- und Südanschluss), ihr 5-m-Mittelstück # zwischen den beiden Kreuzungspunkten fällt nach Regel d weg (beide Enden auf demselben # Knoten) - macht 2 + 2 + 2 = 6. assert out.degree(kreuzungen[0]) == 6 _kanten_enden_auf_ihrem_knoten(out)