Züge fahren flüssig statt zu springen
Server liefert zusätzlich die Position in 10 s, der Browser interpoliert dazwischen (~15 fps). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
@@ -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.
|
- 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
|
- 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.
|
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.
|
- 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
|
- In der Nacht des Fahrplanwechsels (2. Sonntag im Dezember) fehlen bis zum nächsten
|
||||||
stündlichen Lauf die Züge.
|
stündlichen Lauf die Züge.
|
||||||
|
|||||||
+10
-3
@@ -19,6 +19,7 @@ from . import realtime
|
|||||||
from .feed import TZ, Feed
|
from .feed import TZ, Feed
|
||||||
|
|
||||||
log = logging.getLogger("trainpulse")
|
log = logging.getLogger("trainpulse")
|
||||||
|
AHEAD = 10
|
||||||
WEB = Path(__file__).resolve().parent.parent / "web"
|
WEB = Path(__file__).resolve().parent.parent / "web"
|
||||||
|
|
||||||
|
|
||||||
@@ -81,8 +82,14 @@ def poll_realtime():
|
|||||||
def compute_positions():
|
def compute_positions():
|
||||||
if not S.feed:
|
if not S.feed:
|
||||||
return
|
return
|
||||||
feats = S.feed.vehicles(time.time(), S.rt)
|
now = time.time()
|
||||||
S.vehicles = json.dumps({"type": "FeatureCollection", "features": feats}, separators=(",", ":")).encode()
|
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.vehicles_gz = gzip.compress(S.vehicles, 5)
|
||||||
S.count, S.computed_at = len(feats), time.time()
|
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")
|
@app.get("/api/vehicles")
|
||||||
def vehicles(request: Request):
|
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", ""):
|
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_gz, media_type="application/json", headers={**headers, "Content-Encoding": "gzip"})
|
||||||
return Response(S.vehicles, media_type="application/json", headers=headers)
|
return Response(S.vehicles, media_type="application/json", headers=headers)
|
||||||
|
|||||||
+32
-2
@@ -88,14 +88,44 @@ document.getElementById("btn-sat").onclick = () => setBackground(true);
|
|||||||
|
|
||||||
// ---------- Züge ----------
|
// ---------- 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() {
|
async function refreshTrains() {
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch("api/vehicles");
|
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 */ }
|
} 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() {
|
async function refreshStatus() {
|
||||||
const el = document.getElementById("status");
|
const el = document.getElementById("status");
|
||||||
try {
|
try {
|
||||||
@@ -173,8 +203,8 @@ map.on("load", () => {
|
|||||||
setBackground(sat);
|
setBackground(sat);
|
||||||
refreshTrains();
|
refreshTrains();
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
// shortcut: Züge springen alle 5 s; für flüssige Bewegung clientseitig interpolieren
|
|
||||||
setInterval(refreshTrains, 5000);
|
setInterval(refreshTrains, 5000);
|
||||||
|
requestAnimationFrame(animate);
|
||||||
setInterval(refreshStatus, 30000);
|
setInterval(refreshStatus, 30000);
|
||||||
setInterval(() => openId && showTrip(openId), 30000);
|
setInterval(() => openId && showTrip(openId), 30000);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user