Züge folgen dem Gleis statt gerader Linien, Mindesthaltezeit 40 s, osmium renumber

- Server schickt pro Zug die Gleispunkte der nächsten 12 s mit Zeitstempel
  (gerade Stücke auf ~3 m vereinfacht); Browser fährt sie ab, Richtung aus
  dem aktuellen Gleisstück
- Halte mit Ankunft = Abfahrt werden für die Animation auf 40 s verlängert
- OSM-IDs durchnummerieren: osmium extract braucht sonst ~4 GB RAM

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
Claude
2026-10-22 07:29:00 +02:00
parent d339da5da2
commit 7ece7603a9
5 changed files with 118 additions and 34 deletions
+66 -4
View File
@@ -76,6 +76,48 @@ def long_distance(cat: str, line: str) -> bool:
return bool(m) and m.group(0).upper() in LONG_DISTANCE
MIN_DWELL = 40 # s; im Fahrplan steht oft Ankunft = Abfahrt, dann würde der Zug nie sichtbar halten
def with_min_dwell(arr, dep):
"""Halte mit weniger als MIN_DWELL Haltezeit für die Animation verlängern: etwas früher ankommen,
etwas später abfahren – höchstens ein Viertel der Fahrzeit davor/danach."""
pa, pd = list(arr), list(dep)
for k in range(1, len(arr) - 1):
short = MIN_DWELL - (dep[k] - arr[k])
if short > 0:
h = min(short / 2, (arr[k] - dep[k - 1]) / 4, (arr[k + 1] - dep[k]) / 4)
if h > 0:
pa[k], pd[k] = arr[k] - h, dep[k] + h
return pa, pd
def simplify(p: list, tol_m: float, fixed=()) -> list:
"""Douglas-Peucker auf [lon, lat, t]-Punkten (Toleranz in Metern, grob über die Breite umgerechnet).
Punkte in `fixed` bleiben immer erhalten."""
if len(p) < 3:
return p
kx = 111320 * math.cos(math.radians(p[0][1]))
keep = {0, len(p) - 1, *fixed}
cuts = sorted(keep)
stack = list(zip(cuts, cuts[1:]))
while stack:
a, b = stack.pop()
(x1, y1), (x2, y2) = (p[a][0] * kx, p[a][1] * 110540), (p[b][0] * kx, p[b][1] * 110540)
dx, dy = x2 - x1, y2 - y1
n = math.hypot(dx, dy)
best, idx = 0.0, -1
for i in range(a + 1, b):
x, y = p[i][0] * kx, p[i][1] * 110540
d = abs(dy * x - dx * y + x2 * y1 - y2 * x1) / n if n else math.hypot(x - x1, y - y1)
if d > best:
best, idx = d, i
if best > tol_m:
keep.add(idx)
stack += [(a, idx), (idx, b)]
return [p[i] for i in sorted(keep)]
class Feed:
def __init__(self, path: Path, today: dt.date | None = None):
self.path = path
@@ -227,6 +269,23 @@ class Feed:
self._geom_cache[key] = g
return g
def track(self, ti: int, dists: list, step: float, tolerance: float = 3.0) -> list:
"""Gleispunkte von der aktuellen Position bis zur Position in len(dists)-1 Schritten, je mit der
Zeit (s ab jetzt), zu der der Zug dort ist. Gerade Stücke werden auf ~3 m vereinfacht."""
pts, cum, _ = self.geometry(ti)
out, fixed = [], []
for k in range(len(dists)):
d0 = dists[k]
fixed.append(len(out)) # Zeitpunkte alle `step` s behalten, sonst ginge das Bremsen verloren
out.append((d0, k * step))
if k + 1 < len(dists) and dists[k + 1] > d0:
d1 = dists[k + 1]
for j in range(bisect_right(cum, d0), bisect_right(cum, d1)):
if d0 < cum[j] < d1:
out.append((cum[j], (k + (cum[j] - d0) / (d1 - d0)) * step))
res = [[*self.point_at((pts, cum, None), d), round(t, 2)] for d, t in out]
return simplify(res, tolerance, fixed)
@staticmethod
def point_at(g, d: float):
pts, cum, _ = g
@@ -299,14 +358,16 @@ class Feed:
arr, dep = self.times(ti, ds, r[1] if r else None)
if not (dep[0] <= t <= arr[-1]):
continue
i = min(bisect_right(dep, t), len(dep) - 1) # Zug ist zwischen Halt i-1 und i (oder steht an Halt i)
pa, pd = with_min_dwell(arr, dep) # nur für die Position; Zeiten/Verspätung bleiben echt
i = min(bisect_right(pd, t), len(pd) - 1) # Zug ist zwischen Halt i-1 und i (oder steht an Halt i)
g = self.geometry(ti)
sd = g[2]
o = self.off[ti]
if t >= arr[i]: # steht am Halt i
dist, delay = sd[i], dep[i] - self.st_dep[o + i]
if t >= pa[i]: # steht am Halt i
dist = sd[i]
delay = dep[i] - self.st_dep[o + i] if t >= arr[i] else arr[i] - self.st_arr[o + i]
else:
dist = sd[i - 1] + travelled(sd[i] - sd[i - 1], arr[i] - dep[i - 1], t - dep[i - 1])
dist = sd[i - 1] + travelled(sd[i] - sd[i - 1], pa[i] - pd[i - 1], t - pd[i - 1])
delay = arr[i] - self.st_arr[o + i]
lon, lat = self.point_at(g, dist)
# Richtung aus dem Gleisverlauf (±25 m), stimmt auch im Stillstand
@@ -323,6 +384,7 @@ class Feed:
"delay": delay if r else None, "next": self.stop_name[self.st_stop[o + i]],
"heading": heading,
},
"_ti": ti, "_dist": dist, # intern, für track(); werden vor dem Versand entfernt
})
return feats