Trams und U-Bahnen (CH, DE, NL): eigener Filter, ab Stadtzoom im Ausschnitt; NL-Echtzeit auch für Tram/Metro; Vorausschau ohne 7-fache Neuberechnung; deutsche Steig-Zusätze aus Haltestellennamen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
+36
-18
@@ -47,6 +47,9 @@ class State:
|
||||
vehicles: bytes = b'{"type":"FeatureCollection","features":[]}'
|
||||
vehicles_gz: bytes = gzip.compress(vehicles)
|
||||
count = 0
|
||||
# Trams/U-Bahnen: zu viele für eine Gesamtliste -> je Fahrzeug fertiges JSON, ausgeliefert nur
|
||||
# für den Kartenausschnitt
|
||||
urban: tuple = (0.0, []) # (Berechnungszeit, [(lon, lat, json-bytes)]) – ein Objekt, damit beides zusammenpasst
|
||||
computed_at: float | None = None
|
||||
|
||||
|
||||
@@ -99,8 +102,12 @@ def poll_realtime(src: Source):
|
||||
if not src.has_rt or not src.feed:
|
||||
return
|
||||
try:
|
||||
src.rt_version, src.rt = realtime.parse(realtime.fetch(src.cfg["rt_url"], src.cfg["rt_key"]),
|
||||
src.feed.trip_index)
|
||||
merged, version = {}, None
|
||||
for url in src.cfg["rt_url"].split(): # z. B. Niederlande: Züge und Trams in getrennten Feeds
|
||||
v, part = realtime.parse(realtime.fetch(url, src.cfg["rt_key"]), src.feed.trip_index)
|
||||
merged.update(part)
|
||||
version = version or v
|
||||
src.rt_version, src.rt = version, merged
|
||||
src.rt_at, src.rt_error = time.time(), None
|
||||
except Exception as e: # Daten älter als 10 min sind wertlos -> verwerfen statt falsch anzeigen
|
||||
src.rt_error = str(e)
|
||||
@@ -111,9 +118,9 @@ def poll_realtime(src: Source):
|
||||
src.rt_tried_at = time.time()
|
||||
|
||||
|
||||
def positions(src: Source, t: float) -> list:
|
||||
def positions(src: Source, t: float, ahead: tuple = ()) -> list:
|
||||
feats = []
|
||||
for f in src.feed.vehicles(t, src.rt):
|
||||
for f in src.feed.vehicles(t, src.rt, ahead):
|
||||
if len(S.sources) > 1 and not border.shows(src.name, S.sources, *f["geometry"]["coordinates"]):
|
||||
continue # gehört zum Fahrplan des Nachbarlandes
|
||||
f["properties"]["id"] = f"{src.name}/{f['properties']['id']}"
|
||||
@@ -127,18 +134,10 @@ def compute_positions():
|
||||
for src in list(S.sources.values()):
|
||||
if not src.feed:
|
||||
continue
|
||||
cur = positions(src, now)
|
||||
# Distanz entlang der Strecke alle STEP s; daraus der Gleisverlauf, dem der Browser folgt
|
||||
future = [{f["properties"]["id"]: f["_dist"] for f in src.feed.vehicles(now + STEP * (k + 1), src.rt)}
|
||||
for k in range(STEPS)] # Schlüssel Betriebstag/Fahrt: Nachtzüge laufen über Mitternacht
|
||||
# Distanz entlang der Strecke jetzt und alle STEP s; daraus der Gleisverlauf, dem der Browser folgt
|
||||
cur = positions(src, now, tuple(STEP * (k + 1) for k in range(STEPS)))
|
||||
for f in cur:
|
||||
key, dists = f["properties"]["id"].split("/", 1)[1], [f["_dist"]]
|
||||
for pos in future:
|
||||
if key not in pos:
|
||||
break # Fahrt endet
|
||||
dists.append(pos[key])
|
||||
f["path"] = src.feed.track(f.pop("_ti"), dists, STEP)
|
||||
del f["_dist"]
|
||||
f["path"] = src.feed.track(f.pop("_ti"), [f.pop("_dist"), *f.pop("_ahead")], STEP)
|
||||
feats += cur
|
||||
# Im übrigen Ausland (Luxemburg, Österreich, …) zeigen alle Fahrpläne ihre Züge -> dort Doppelte
|
||||
# (gleiche Linie, gleiche ~500-m-Zelle) entfernen, bevorzugt den mit Echtzeit behalten
|
||||
@@ -151,11 +150,13 @@ def compute_positions():
|
||||
continue
|
||||
seen.add((line, cx, cy))
|
||||
out.append(f)
|
||||
feats = out
|
||||
S.vehicles = json.dumps({"type": "FeatureCollection", "ts": now, "features": feats},
|
||||
rail = [f for f in out if f["properties"]["mode"] == "rail"]
|
||||
S.urban = (now, [(*f["geometry"]["coordinates"], json.dumps(f, separators=(",", ":")).encode())
|
||||
for f in out if f["properties"]["mode"] != "rail"])
|
||||
S.vehicles = json.dumps({"type": "FeatureCollection", "ts": now, "features": rail},
|
||||
separators=(",", ":")).encode()
|
||||
S.vehicles_gz = gzip.compress(S.vehicles, 5)
|
||||
S.count, S.computed_at = len(feats), time.time()
|
||||
S.count, S.computed_at = len(rail), time.time()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -190,6 +191,22 @@ def vehicles(request: Request):
|
||||
return Response(S.vehicles, media_type="application/json", headers=headers)
|
||||
|
||||
|
||||
@app.get("/api/urban")
|
||||
def urban(request: Request, bbox: str):
|
||||
"""Trams und U-Bahnen im Ausschnitt "west,süd,ost,nord"."""
|
||||
try:
|
||||
w, s, e, n = (float(v) for v in bbox.split(","))
|
||||
except ValueError:
|
||||
raise HTTPException(400, "bbox=west,süd,ost,nord")
|
||||
ts, items = S.urban
|
||||
feats = [j for x, y, j in items if w <= x <= e and s <= y <= n]
|
||||
body = b'{"type":"FeatureCollection","ts":%.3f,"features":[%s]}' % (ts, b",".join(feats))
|
||||
headers = {"Cache-Control": "no-store", "X-Now": str(time.time())}
|
||||
if "gzip" in request.headers.get("accept-encoding", ""):
|
||||
return Response(gzip.compress(body, 5), media_type="application/json", headers={**headers, "Content-Encoding": "gzip"})
|
||||
return Response(body, media_type="application/json", headers=headers)
|
||||
|
||||
|
||||
@app.get("/api/trip/{source}/{date}/{ti}")
|
||||
def trip(source: str, date: str, ti: int):
|
||||
src = S.sources.get(source)
|
||||
@@ -211,6 +228,7 @@ def status():
|
||||
"ready": bool(loaded and not pending and S.computed_at
|
||||
and all(S.computed_at >= s.loaded_at for s in loaded) and all(s.rt_tried_at and S.computed_at >= s.rt_tried_at for s in rt)),
|
||||
"vehicles": S.count,
|
||||
"urban": len(S.urban[1]),
|
||||
"rt_ok": bool(ok) and len(ok) == len(loaded),
|
||||
"rt_some": bool(ok),
|
||||
"sources": {s.name: {
|
||||
|
||||
Reference in New Issue
Block a user