fix(extract): Chunk-Ausgabe lauf-stabil, Eckenprüfung in beide Richtungen
emit.py normalisiert jede ausgegebene Fläche und sortiert die Listen nach der Ausgabestruktur selbst. unary_union() in apply_street() gab das kartenweite Straßenpolygon mit anderem Ringstartpunkt und an anderer Listenstelle zurück und änderte damit 88 Chunkdateien, in denen sich geometrisch nichts bewegt hatte (symmetrische Differenz 0,00 m²). Belagsarten und Mittellinien behalten ihre Reihenfolge: overview.py malt die Beläge in Listenreihenfolge übereinander, und vergeLampPoints() hängt an Reihenfolge und Laufrichtung der Mittellinien — Sortieren verschöbe dort 1.848 von 3.921 Laternen auf die andere Straßenseite. check_corners() misst gegen den Straßenrand statt gegen die Straßenfläche. Alle vier Ecken liegen in der Fahrbahn, ihr Flächenabstand ist 0,000 m; die alte Prüfung schlug deshalb nur an, wenn eine Straße sich zurückzieht. Eine nach innen verbreiterte Straße hätte apply_park() still auf das veraltete Viereck zurückgeschnitten. Die Parkwege werden am Parkviereck abgeschnitten — die runden Kappen von LineString.buffer() lagen mit 95,96 m² darüber hinaus, rund 41 m² davon sichtbar als Betonzungen auf den umgebenden Belägen. load_elements() prüft den OSM-Abzug so streng wie die Parkdatei, mit Dateiname statt nacktem KeyError. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a3504df43d
commit
3c74202b5e
10 files changed
+333
-102
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
from itertools import groupby
|
||||
from pathlib import Path
|
||||
|
||||
import shapely
|
||||
from shapely.geometry import Polygon, box
|
||||
|
||||
from .coords import CHUNK_SIZE, SLICE
|
||||
@@ -31,29 +33,76 @@ def _rings(poly: Polygon, i: int, j: int) -> list[list[list[float]]]:
|
||||
im Park) ist genauso echte Flaeche wie der Aussenring und darf beim Zuschneiden nicht
|
||||
verschwinden - sonst wird der Block zu Asphalt, der Innenhof zu Gebaeudemasse, der Teich
|
||||
zu Wiese.
|
||||
|
||||
normalize() zuerst, weil dieselbe Flaeche sonst je nach Rechenweg anders in der Datei
|
||||
steht: GEOS waehlt Ringstartpunkt und Wicklung nach Belieben. unary_union() in
|
||||
apply_street() fasst dafuer das kartenweite Strassenpolygon neu zusammen und aenderte so
|
||||
88 Chunkdateien, in denen sich geometrisch nichts bewegt hatte - symmetrische Differenz
|
||||
der Strassenvereinigung exakt 0,00 m2 in allen 88. normalize() legt beides fest
|
||||
(lexikographisch kleinster Punkt zuerst, Aussenring im Uhrzeigersinn, Loecher dagegen)
|
||||
und macht die Ausgabe damit vom Rechenweg unabhaengig.
|
||||
|
||||
Die vereinheitlichte Wicklung ist unbedenklich, nachgeprueft an allen drei Verbrauchern:
|
||||
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).
|
||||
"""
|
||||
ox = SLICE["x0"] + i * CHUNK_SIZE
|
||||
oy = SLICE["y1"] - j * CHUNK_SIZE
|
||||
poly = shapely.normalize(poly)
|
||||
# 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)]
|
||||
|
||||
|
||||
def _sorted_surfaces(entries: list[dict]) -> list[dict]:
|
||||
"""Belaege nur innerhalb einer Art sortieren - zwischen den Arten traegt die Reihenfolge Bedeutung.
|
||||
|
||||
overview.py malt die Belaege in Listenreihenfolge uebereinander, der spaetere gewinnt. An den
|
||||
ausgelieferten Chunks ueberlappen sich verschiedene Arten auf 36.873 m2: der Parkrasen liegt
|
||||
unter Feldern (19.573 m2), Plaetzen (11.872 m2), Parkplatz (2.878 m2), Pool (539 m2) und
|
||||
Spielplatzsand (270 m2), die Stadtflaeche ueber dem Strandsand (1.729 m2). Ein Sortieren ueber
|
||||
die Arten hinweg kehrte das um und liesse die Felder unter dem Rasen verschwinden - und eine
|
||||
feste Rangfolge je Art gaebe es dafuer nicht, weil "sand" als Strand vor und als Spielplatz
|
||||
hinter "park" steht. Die Abfolge der Arten selbst steht fest im Code (run.py, SURFACE_ORDER in
|
||||
park.py) und ist damit schon eindeutig; GEOS mischt nur innerhalb einer Art.
|
||||
"""
|
||||
return [entry for _, run in groupby(entries, key=lambda e: e["kind"])
|
||||
for entry in sorted(run, key=lambda e: e["poly"])]
|
||||
|
||||
|
||||
def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms, paths=()) -> dict:
|
||||
"""Ein Chunk als Ausgabestruktur, deterministisch sortiert.
|
||||
|
||||
Sortiert wird nach der gerundeten Ausgabestruktur selbst, nicht nach Flaeche, Schwerpunkt oder
|
||||
Bounding-Box: nur sie ist total geordnet und ohne Sonderfall, denn zwei Eintraege mit gleichem
|
||||
Schluessel sind dann bytegleich und ihre Reihenfolge wieder egal. Die abgeleiteten Zahlen
|
||||
koennen sich dagegen zwei verschiedene Flaechen teilen, und die Ausgabe hinge fuer diese
|
||||
beiden weiterhin an der Listenposition, die GEOS vergibt.
|
||||
|
||||
centerlines bleiben bewusst unsortiert und unnormalisiert: dort traegt sowohl die Reihenfolge
|
||||
als auch die Laufrichtung Bedeutung. vergeLampPoints() in props.js setzt jede Laterne
|
||||
abwechselnd links und rechts der Fahrbahn (gerader Index links, ungerader rechts) und verwirft
|
||||
sie, wenn auf ihrer Seite kein freier Platz ist - an den ausgelieferten Chunks wechselten beim
|
||||
Sortieren 1.848 von 3.921 Laternen die Strassenseite. Die Mittellinien sind auch ohne
|
||||
Sortieren stabil: sie entstehen aus centerlines() und difference(), nie aus unary_union().
|
||||
"""
|
||||
ox = SLICE["x0"] + i * CHUNK_SIZE
|
||||
oy = SLICE["y1"] - j * CHUNK_SIZE
|
||||
return {
|
||||
"chunk": [i, j],
|
||||
"origin": [ox, -oy],
|
||||
"land": [_rings(p, i, j) for p in land],
|
||||
"surfaces": [{"poly": _rings(p, i, j), "kind": k} for p, k in surfaces],
|
||||
"roads": [{"poly": _rings(p, i, j), "bridge": b} for p, b in roads],
|
||||
"land": sorted(_rings(p, i, j) for p in land),
|
||||
"surfaces": _sorted_surfaces([{"poly": _rings(p, i, j), "kind": k} for p, k in surfaces]),
|
||||
"roads": sorted(({"poly": _rings(p, i, j), "bridge": b} for p, b in roads),
|
||||
key=lambda r: (r["poly"], r["bridge"])),
|
||||
"centerlines": [[[round(x - ox, 2), round(-y + oy, 2)] for x, y in ln.coords] for ln in centerlines],
|
||||
"buildings": [{"poly": _rings(p, i, j), "height": h["height"],
|
||||
"source": h["source"], "speculative": s} for p, h, s in buildings],
|
||||
"palms": [[round(x - ox, 2), round(-y + oy, 2)] for x, y in palms],
|
||||
"buildings": sorted(({"poly": _rings(p, i, j), "height": h["height"],
|
||||
"source": h["source"], "speculative": s} for p, h, s in buildings),
|
||||
key=lambda b: (b["poly"], b["height"], b["source"], b["speculative"])),
|
||||
"palms": sorted([round(x - ox, 2), round(-y + oy, 2)] for x, y in palms),
|
||||
# Parkwege (Flamingo Park, docs/superpowers/specs/2026-09-15-flamingo-park-design.md), aufgebaut wie roads.
|
||||
"paths": [{"poly": _rings(p, i, j)} for p in paths],
|
||||
"paths": sorted(({"poly": _rings(p, i, j)} for p in paths), key=lambda p: p["poly"]),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user