From efeba65fb1654bed8d5eae70e41ae2e4047506e0 Mon Sep 17 00:00:00 2001 From: Guido Date: Wed, 21 Oct 2026 15:00:12 +0200 Subject: [PATCH] =?UTF-8?q?Z=C3=BCge=20fahren=20fl=C3=BCssig=20statt=20zu?= =?UTF-8?q?=20springen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server liefert zusätzlich die Position in 10 s, der Browser interpoliert dazwischen (~15 fps). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb --- README.md | 3 ++- trainpulse/server.py | 13 ++++++++++--- web/app.js | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 355805e..f614840 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ Die Hinweise erscheinen unten rechts auf der Karte. - Nur Bahn. Busse, Trams und Schiffe sind im Fahrplan, werden aber noch herausgefiltert. - Der Schweizer Fahrplan enthält auch einige ausländische Züge (z. B. TGV, TER); deren Strecke ausserhalb der Schweiz ist nur Luftlinie, weil der OSM-Ausschnitt nur die Schweiz abdeckt. -- Züge springen alle 5 Sekunden zur neuen Position statt flüssig zu fahren. +- Zwischen zwei Abfragen fahren die Züge auf gerader Linie; in engen Kurven schneiden sie + für ein paar Sekunden minimal ab. - Zusätzliche Fahrten, die nur in den Echtzeitdaten vorkommen (Extrazüge), fehlen. - In der Nacht des Fahrplanwechsels (2. Sonntag im Dezember) fehlen bis zum nächsten stündlichen Lauf die Züge. diff --git a/trainpulse/server.py b/trainpulse/server.py index 0508b64..5230b66 100644 --- a/trainpulse/server.py +++ b/trainpulse/server.py @@ -19,6 +19,7 @@ from . import realtime from .feed import TZ, Feed log = logging.getLogger("trainpulse") +AHEAD = 10 WEB = Path(__file__).resolve().parent.parent / "web" @@ -81,8 +82,14 @@ def poll_realtime(): def compute_positions(): if not S.feed: return - feats = S.feed.vehicles(time.time(), S.rt) - S.vehicles = json.dumps({"type": "FeatureCollection", "features": feats}, separators=(",", ":")).encode() + now = time.time() + feats = S.feed.vehicles(now, S.rt) + # Position in AHEAD s mitliefern: der Browser fährt die Züge damit flüssig dazwischen + ahead = {f["properties"]["id"]: f["geometry"]["coordinates"] for f in S.feed.vehicles(now + AHEAD, S.rt)} + for f in feats: + f["next"] = ahead.get(f["properties"]["id"]) + S.vehicles = json.dumps({"type": "FeatureCollection", "ts": now, "ahead": AHEAD, "features": feats}, + separators=(",", ":")).encode() S.vehicles_gz = gzip.compress(S.vehicles, 5) S.count, S.computed_at = len(feats), time.time() @@ -102,7 +109,7 @@ app = FastAPI(lifespan=lifespan, docs_url=None, redoc_url=None) @app.get("/api/vehicles") def vehicles(request: Request): - headers = {"Cache-Control": "no-store"} + headers = {"Cache-Control": "no-store", "X-Now": str(time.time())} # für den Uhrenabgleich im Browser if "gzip" in request.headers.get("accept-encoding", ""): return Response(S.vehicles_gz, media_type="application/json", headers={**headers, "Content-Encoding": "gzip"}) return Response(S.vehicles, media_type="application/json", headers=headers) diff --git a/web/app.js b/web/app.js index 4c9b96a..67277b1 100644 --- a/web/app.js +++ b/web/app.js @@ -88,14 +88,44 @@ document.getElementById("btn-sat").onclick = () => setBackground(true); // ---------- Züge ---------- +// Server liefert je Zug die Position jetzt und in `ahead` Sekunden; dazwischen wird linear gefahren. +let trains = { type: "FeatureCollection", features: [] }; +let ts = 0, ahead = 10, clockOffset = 0; + async function refreshTrains() { if (document.hidden) return; try { const r = await fetch("api/vehicles"); - if (r.ok) map.getSource("trains").setData(await r.json()); + if (!r.ok) return; + const now = Number(r.headers.get("X-Now")); + const d = await r.json(); + if (now) clockOffset = Date.now() / 1000 - now; + for (const f of d.features) f.start = f.geometry.coordinates; + trains = d; + ts = d.ts; + ahead = d.ahead || 10; } catch (e) { /* nächster Versuch in ein paar Sekunden */ } } +function render() { + // leicht über `ahead` hinaus weiterfahren, falls eine Abfrage hängt, dann anhalten + const k = Math.max(0, Math.min((Date.now() / 1000 - clockOffset - ts) / ahead, 1.5)); + for (const f of trains.features) { + if (!f.next) continue; + f.geometry.coordinates = [f.start[0] + (f.next[0] - f.start[0]) * k, f.start[1] + (f.next[1] - f.start[1]) * k]; + } + map.getSource("trains").setData(trains); +} + +let lastFrame = 0; +function animate(ms) { + if (!document.hidden && ms - lastFrame > 66) { // ~15 Bilder/s reichen und schonen Akku + lastFrame = ms; + render(); + } + requestAnimationFrame(animate); +} + async function refreshStatus() { const el = document.getElementById("status"); try { @@ -173,8 +203,8 @@ map.on("load", () => { setBackground(sat); refreshTrains(); refreshStatus(); - // shortcut: Züge springen alle 5 s; für flüssige Bewegung clientseitig interpolieren setInterval(refreshTrains, 5000); + requestAnimationFrame(animate); setInterval(refreshStatus, 30000); setInterval(() => openId && showTrip(openId), 30000); });