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:
knso committed 2026-09-18 06:47:14 +02:00
1 parent 83140f89e1
commit a397892847
2 files changed
+74 -2

No files matched your search

+26 -1
View File
@@ -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
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).
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
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
return [[[round(x - ox, 2), round(-y - (-oy), 2)] for x, y in ring.coords]
for ring in (poly.exterior, *poly.interiors)]