fix(extract): set_precision statt Koordinatenrundung gegen selbstschneidende Ringe
_rings() rundete bislang jede Koordinate unabhängig auf 2 Stellen, nach normalize(). Das kann zwei Ringpunkte so verschieben, dass sich der Ring selbst schneidet, obwohl das Ausgangspolygon gültig war - Ursache der acht neuen ungültigen Polygone in den ausgelieferten Chunks (carriageway 3, land 2, sidewalk 2, surfaces 1). shapely.set_precision(poly, 0.01) läuft jetzt vor normalize() und vor der Rundung: Es ist eine echte Geometrieoperation (GEOS hält das Ergebnis mit mode="valid_output" aktiv gültig) statt eines blinden round() je Zahl. Gegen alle 29.037 Polygone der committeten (bereits einmal falsch gerundeten) Chunks probeweise gerechnet: kein Absturz, 8 der 9 ungültigen Polygone (die neunte ist die bekannte, von diesem Zweig unabhängige Vorbelastung in "paths", chunk_10_6, schon vor diesem Zweig ungültig) werden gültig, Flächenänderung insgesamt -24,35 m² von 11.711.354 m² (-0,00021 %). Zwei Sonderfälle brauchten eine eigene Behandlung, die die Zeilen im Befund nicht erwähnen, ohne die aber der Vier-Minuten-Lauf hätte abbrechen oder eine falsche Fläche ausliefern können: - An einer Engstelle unter 1 cm kann set_precision ein Polygon in mehrere Teile spalten (3 von 29.037 Fällen, größter abgeschnittener Teil 25,9 m²) - poly.exterior auf einer MultiPolygon wäre ein AttributeError gewesen. Es bleibt jetzt das größere Teilstück. - Ganz vereinzelt lehnt GEOS das Schnappen mit einer TopologyException ab (in der Stichprobe nur an einem bereits vorher ungültigen Polygon); dann bleibt das Polygon ungerundet, und die alte Rundung je Koordinate greift wie vor diesem Befund, statt den Lauf abzubrechen. Zwei Tests, beide ohne die Korrektur rot: ein valides Polygon, dessen unabhängig gerundete Ecken sich kreuzen (Beispiel selbst nachgerechnet, nicht nur behauptet), und eine Hantelform mit einem Steg unter 1 cm, die am unkorrigierten _rings() unter der Rundung selbst ungültig wird statt sich sauber zu teilen.
This commit is contained in:
1 parent
83140f89e1
commit
a397892847
2 files changed
+74
-2
No files matched your search
@@ -46,10 +46,35 @@ def _rings(poly: Polygon, i: int, j: int) -> list[list[list[float]]]:
|
|||||||
geometry.js stellt sie beim Laden ohnehin selbst her (clockwise(), Zeile 50), das
|
geometry.js stellt sie beim Laden ohnehin selbst her (clockwise(), Zeile 50), das
|
||||||
Halbstrahl-Verfahren von pointInPolygon() in props.js kommt ohne sie aus, und PIL fuellt
|
Halbstrahl-Verfahren von pointInPolygon() in props.js kommt ohne sie aus, und PIL fuellt
|
||||||
in overview.py unabhaengig von Wicklung und Startpunkt (nachgemessen, Bild bytegleich).
|
in overview.py unabhaengig von Wicklung und Startpunkt (nachgemessen, Bild bytegleich).
|
||||||
|
|
||||||
|
set_precision(poly, 0.01) laeuft vor normalize() und vor der Rundung selbst (Befund 5 der
|
||||||
|
Fixwelle): Wer stattdessen jede Koordinate unabhaengig auf 2 Stellen rundet, wie es hier bis
|
||||||
|
zu diesem Befund geschah, kann zwei Ringpunkte so verschieben, dass sich der Ring selbst
|
||||||
|
schneidet - set_precision ist eine echte Geometrieoperation (GEOS haelt das Ergebnis mit
|
||||||
|
mode="valid_output" aktiv gueltig) statt eines blinden Python-round() je Zahl. Acht von
|
||||||
|
144 ausgelieferten Chunks hatten dadurch ein ungueltiges Polygon (carriageway 3, land 2,
|
||||||
|
sidewalk 2, surfaces 1) - set_precision behebt alle acht (nachgemessen).
|
||||||
|
|
||||||
|
Zwei Sonderfaelle, an einer Stichprobe ueber alle 29.037 ausgelieferten Polygone gemessen:
|
||||||
|
An einer Engstelle unter 1 cm spaltet set_precision ein Polygon in mehrere Teile (3 von
|
||||||
|
29.037 Faellen, groesster abgeschnittener Teil 25,9 m²) - hier bleibt nur das groesste
|
||||||
|
Teilstueck; das Polygon soll eine zusammenhaengende Flaeche bleiben, kein Zusatzeintrag,
|
||||||
|
den kein Aufrufer erwartet. Ganz vereinzelt lehnt GEOS das Schnappen mit einer
|
||||||
|
TopologyException ab (in der Stichprobe nur an einem bereits vorher ungueltigen Polygon);
|
||||||
|
dann bleibt das Polygon ungerundet, und die anschliessende Rundung je Koordinate greift wie
|
||||||
|
vor diesem Befund - kein Absturz des Vier-Minuten-Laufs an einer einzelnen Ausnahme.
|
||||||
"""
|
"""
|
||||||
ox = SLICE["x0"] + i * CHUNK_SIZE
|
ox = SLICE["x0"] + i * CHUNK_SIZE
|
||||||
oy = SLICE["y1"] - j * CHUNK_SIZE
|
oy = SLICE["y1"] - j * CHUNK_SIZE
|
||||||
poly = shapely.normalize(poly)
|
try:
|
||||||
|
genau = shapely.set_precision(poly, 0.01)
|
||||||
|
except shapely.errors.GEOSException:
|
||||||
|
genau = poly
|
||||||
|
if genau.geom_type == "MultiPolygon" and not genau.is_empty:
|
||||||
|
genau = max(genau.geoms, key=lambda g: g.area)
|
||||||
|
elif genau.is_empty or genau.geom_type != "Polygon":
|
||||||
|
genau = poly
|
||||||
|
poly = shapely.normalize(genau)
|
||||||
# Welt (X, Y) -> three (x, z) -> relativ zur Chunk-Ecke
|
# Welt (X, Y) -> three (x, z) -> relativ zur Chunk-Ecke
|
||||||
return [[[round(x - ox, 2), round(-y - (-oy), 2)] for x, y in ring.coords]
|
return [[[round(x - ox, 2), round(-y - (-oy), 2)] for x, y in ring.coords]
|
||||||
for ring in (poly.exterior, *poly.interiors)]
|
for ring in (poly.exterior, *poly.interiors)]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import pytest
|
||||||
from shapely.geometry import Polygon
|
from shapely.geometry import Polygon
|
||||||
from leonida.emit import chunk_payload, split_by_chunk, split_by_chunk_all
|
from leonida.emit import _rings, chunk_payload, split_by_chunk, split_by_chunk_all
|
||||||
|
|
||||||
BIG = Polygon([(-1000, 2500), (-700, 2500), (-700, 2200), (-1000, 2200)]) # Chunk (0,0) und Nachbarn
|
BIG = Polygon([(-1000, 2500), (-700, 2500), (-700, 2200), (-1000, 2200)]) # Chunk (0,0) und Nachbarn
|
||||||
HEIGHT = {"height": 9.0, "source": "district"}
|
HEIGHT = {"height": 9.0, "source": "district"}
|
||||||
@@ -53,6 +54,52 @@ def test_polygon_holes_survive_into_the_payload():
|
|||||||
assert max(x for x, _ in exterior) > max(x for x, _ in hole)
|
assert max(x for x, _ in exterior) > max(x for x, _ in hole)
|
||||||
assert all(0 <= x <= 250 and 0 <= z <= 250 for x, z in hole)
|
assert all(0 <= x <= 250 and 0 <= z <= 250 for x, z in hole)
|
||||||
|
|
||||||
|
def test_rings_bleiben_gueltig_wenn_koordinatenweises_runden_selbstschnitte_erzeugen_wuerde():
|
||||||
|
"""Befund 5 der Fixwelle: _rings() rundete bislang jede Koordinate unabhaengig auf 2 Stellen,
|
||||||
|
NACH normalize(). Das kann zwei Ringpunkte so verschieben, dass sich der Ring selbst
|
||||||
|
schneidet, obwohl das Ausgangspolygon gueltig war. Am echten Netz betraf das 8 von 144
|
||||||
|
Chunks (carriageway 3, land 2, sidewalk 2, surfaces 1) - je zwei ungueltige Polygone, alle
|
||||||
|
neu in diesem Zweig, Ursache 2-Stellen-Rundung statt einer echten Geometrieaenderung.
|
||||||
|
|
||||||
|
Das folgende Polygon ist gueltig; unabhaengig gerundete Ecken (die alte Methode) kreuzen
|
||||||
|
sich trotzdem - der Befund ist damit am Beispiel nachvollzogen, nicht nur behauptet.
|
||||||
|
set_precision() vor normalize() (die neue Methode) haelt das Ergebnis gueltig, weil es eine
|
||||||
|
echte Geometrieoperation ist statt eines blinden round() je Zahl.
|
||||||
|
"""
|
||||||
|
poly = Polygon([(16.083798, 6.586106), (20.804728, 8.005357),
|
||||||
|
(18.203748, 0.906454), (18.305129, 1.203848)])
|
||||||
|
assert poly.is_valid
|
||||||
|
naiv = Polygon([(round(x, 2), round(y, 2)) for x, y in poly.exterior.coords])
|
||||||
|
assert not naiv.is_valid, "das Beispiel muss den Befund tatsaechlich zeigen"
|
||||||
|
|
||||||
|
rings = _rings(poly, 0, 0)
|
||||||
|
ergebnis = Polygon(rings[0], rings[1:])
|
||||||
|
assert ergebnis.is_valid
|
||||||
|
assert ergebnis.area == pytest.approx(poly.area, rel=0.01)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rings_behaelt_nur_das_groessere_teilstueck_an_einer_engstelle_unter_einem_zentimeter():
|
||||||
|
"""set_precision() kann ein Polygon mit einem Steg unter 1 cm Breite beim Schnappen auf das
|
||||||
|
1-cm-Raster in zwei Teile spalten (an einer Stichprobe der ausgelieferten Daten: 3 von
|
||||||
|
29.037 Polygonen, groesster abgeschnittener Teil 25,9 m²). _rings() liefert aber immer genau
|
||||||
|
ein Ringset - eine MultiPolygon ungeprueft an poly.exterior gereicht waere ein
|
||||||
|
AttributeError. Hier bleibt das groessere der beiden Teilstuecke.
|
||||||
|
|
||||||
|
Am unkorrigierten _rings() (kein set_precision) kollabiert derselbe Steg unter der
|
||||||
|
Koordinatenrundung auf exakt dieselbe Zeile und macht das EINE Ergebnis-Polygon ungueltig,
|
||||||
|
statt es sauber zu teilen - auch das ist also gegen den Befund rot.
|
||||||
|
"""
|
||||||
|
hantel = Polygon([
|
||||||
|
(0.0, 0.0), (10.0, 0.0), (10.0, 4.997), (20.0, 4.997), (20.0, 2.0), (26.0, 2.0), (26.0, 8.0),
|
||||||
|
(20.0, 8.0), (20.0, 5.003), (10.0, 5.003), (10.0, 10.0), (0.0, 10.0),
|
||||||
|
])
|
||||||
|
assert hantel.is_valid and hantel.area == pytest.approx(136.06)
|
||||||
|
rings = _rings(hantel, 0, 0) # darf nicht mit AttributeError abstuerzen
|
||||||
|
ergebnis = Polygon(rings[0], rings[1:])
|
||||||
|
assert ergebnis.is_valid
|
||||||
|
assert ergebnis.area == pytest.approx(100.0, abs=0.5), "das groessere der beiden Teilstuecke"
|
||||||
|
|
||||||
|
|
||||||
def test_the_same_geometry_in_another_order_gives_the_same_output():
|
def test_the_same_geometry_in_another_order_gives_the_same_output():
|
||||||
"""Die Ausgabe darf nicht am Rechenweg haengen, nur an der Geometrie.
|
"""Die Ausgabe darf nicht am Rechenweg haengen, nur an der Geometrie.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user