Spanien (Renfe + Cercanías), Polen, Norwegen, Dänemark, Österreich (ÖBB + Wiener Linien), Schweden (Key)
- mehrere Quellen je Land (country), Grenzen aus Natural Earth, OSM und Kachelausschnitt erweitert - Fahrpläne glätten: Unterordner im Zip, aufgefüllte Felder, fehlende parent_station, level_id - Gattung aus der Zugnummer (ÖBB 'RJ 658'), Fernverkehr auch über Routentyp 101/102/105 - Echtzeit: Verspätung vor Uhrzeit (Renfe), Fahrten ohne Datum, Key in der Adresse (Trafiklab) - httpx-Logs aus (würden Keys zeigen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
+35
-15
@@ -60,7 +60,13 @@ def travelled(length: float, duration: float, t: float) -> float:
|
||||
|
||||
# Gattungen des Fernverkehrs; alles andere (RE, RB, S, R, TER, …) gilt als Nahverkehr
|
||||
LONG_DISTANCE = {"ICE", "IC", "ICN", "ICD", "EC", "ECE", "ECM", "EN", "NJ", "RJ", "RJX", "TGV", "OUIGO", "LYRIA",
|
||||
"THA", "EST", "EUROSTAR", "EUROCITY", "NIGHTJET", "EUROPEAN", "FLX", "IR", "PE", "D"}
|
||||
"THA", "EST", "EUROSTAR", "EUROCITY", "NIGHTJET", "EUROPEAN", "FLX", "IR", "PE", "D",
|
||||
"AVE", "AVLO", "ALVIA", "EUROMED", # Spanien
|
||||
"EIC", "EIP", "TLK", # Polen
|
||||
"ICL", "WB", "SJ", "SNT", "X"} # Dänemark (IC Lyn), Westbahn, Schweden (SJ, Snälltåget, X2000)
|
||||
# Zugnummer mit Gattung ("RJ 658"): bei ÖBB steht der Fernverkehr nur so, die Linie heisst "A10-1"
|
||||
TRIP_CAT = re.compile(r"^([A-Z]{1,5}) ?(\d+)$")
|
||||
NO_FJERNTOG = re.compile(r"^F\d+$") # Norwegen: F4, F5, F6 = Fernzüge (aber dänische S-Bahn "F" nicht)
|
||||
# Frankreich (SNCF): Gattung steht nur in der Trip-ID ("…_F:OUI:…"), die Linie heisst oft "INCONNU"
|
||||
FR_CATEGORY = {"OUI": "TGV", "OGO": "OUIGO", "LYR": "Lyria", "ICE": "ICE", "IC": "IC", "ICN": "IC",
|
||||
"TER": "TER", "CTE": "TER", "NAV": "Navette", "TRN": "TER", "CRE": "TER"}
|
||||
@@ -87,16 +93,16 @@ def route_mode(route_type: str) -> str | None:
|
||||
SBAHN = re.compile(r"^(SN? ?\d+[A-Z]?|S)$") # S1, S 5, S41, SN7 (Nacht-S-Bahn), nur "S"
|
||||
|
||||
|
||||
def sbahn(cat: str, line: str) -> bool:
|
||||
"""S-Bahn: Schweiz über die Kategorie, Deutschland/Belgien über den Liniennamen. Niederländische
|
||||
Sprinter und französische TER zählen als Regionalzug."""
|
||||
return cat in ("S", "SN") or bool(SBAHN.match(line))
|
||||
def sbahn(cat: str, line: str, route_type: str = "") -> bool:
|
||||
"""S-Bahn: Schweiz über die Kategorie, Deutschland/Belgien über den Liniennamen, Dänemark (S-tog)
|
||||
über den Routentyp 109. Niederländische Sprinter und französische TER zählen als Regionalzug."""
|
||||
return cat in ("S", "SN") or route_type == "109" or bool(SBAHN.match(line))
|
||||
|
||||
|
||||
def long_distance(cat: str, line: str) -> bool:
|
||||
# Schweiz: Gattung in route_desc; gtfs.de: nur in der Linie ("ICE 75", "IC 3")
|
||||
m = re.match(r"[A-Za-z]+", cat or line or "")
|
||||
return bool(m) and m.group(0).upper() in LONG_DISTANCE
|
||||
return bool(m) and m.group(0).upper() in LONG_DISTANCE or bool(NO_FJERNTOG.match(line or ""))
|
||||
|
||||
|
||||
MIN_DWELL = 40 # s; im Fahrplan steht oft Ankunft = Abfahrt, dann würde der Zug nie sichtbar halten
|
||||
@@ -142,7 +148,7 @@ def simplify(p: list, tol_m: float, fixed=()) -> list:
|
||||
|
||||
|
||||
class Feed:
|
||||
def __init__(self, path: Path, today: dt.date | None = None):
|
||||
def __init__(self, path: Path, today: dt.date | None = None, all_sbahn: bool = False):
|
||||
self.path = path
|
||||
self.meta = json.loads((path / "meta.json").read_text())
|
||||
self.version = self.meta["feed_version"]
|
||||
@@ -163,11 +169,13 @@ class Feed:
|
||||
self.routes = {}
|
||||
for h, r in read(path / "routes.txt"):
|
||||
line = LINE_PREFIX.sub("", r[h["route_short_name"]] or r[h.get("route_long_name", 0)])
|
||||
if len(line) > 12: # Entur: "Ø Göteborg-Halmstad-…" -> "Ø"; Kartenbeschriftung bleibt kurz
|
||||
line = line.split()[0][:12]
|
||||
self.routes[r[h["route_id"]]] = {
|
||||
"line": LINE_ALIAS.get(line, "" if line == "INCONNU" else line),
|
||||
"cat": r[h["route_desc"]] if "route_desc" in h else "",
|
||||
"agency": agencies.get(r[h["agency_id"]], "") if "agency_id" in h else "",
|
||||
"mode": route_mode(r[h["route_type"]]) or "rail",
|
||||
"mode": route_mode(r[h["route_type"]]) or "rail", "type": r[h["route_type"]],
|
||||
}
|
||||
|
||||
self.trip_index, self.trip_ids, self.trip_route, self.trip_service = {}, [], [], []
|
||||
@@ -188,8 +196,15 @@ class Feed:
|
||||
self.trip_number[-1], self.trip_headsign[-1] = self.trip_headsign[-1], ""
|
||||
for route in self.routes.values():
|
||||
route["line"] = route["line"] or route["cat"]
|
||||
route["fv"] = route["mode"] == "rail" and long_distance(route["cat"], route["line"])
|
||||
route["sb"] = route["mode"] == "rail" and not route["fv"] and sbahn(route["cat"], route["line"])
|
||||
# Routentyp 101/102/105 (Hochgeschwindigkeit, Fern-, Nachtzug): Schweden kennt sonst nur Nummern
|
||||
route["fv"] = route["mode"] == "rail" and (long_distance(route["cat"], route["line"])
|
||||
or route["type"] in ("101", "102", "105"))
|
||||
route["sb"] = route["mode"] == "rail" and not route["fv"] and (all_sbahn or sbahn(route["cat"], route["line"], route["type"]))
|
||||
self.trip_cat = {} # Fahrt -> Gattung aus der Zugnummer, wo die Linie keine Fernverkehrs-Gattung hat
|
||||
for ti, num in enumerate(self.trip_number):
|
||||
m = TRIP_CAT.match(num) if num and not num.isdigit() else None
|
||||
if m and m.group(1) in LONG_DISTANCE and not self.routes[self.trip_route[ti]]["fv"]:
|
||||
self.trip_cat[ti], self.trip_number[ti] = m.group(1), m.group(2)
|
||||
|
||||
# Haltezeiten flach in Arrays; off[i]..off[i+1] sind die Halte von Fahrt i
|
||||
n = len(self.trip_route)
|
||||
@@ -382,7 +397,7 @@ class Feed:
|
||||
break # Liste ist nach Abfahrt sortiert
|
||||
if end + 7200 < t: # grosszügig, damit verspätete Züge nicht verschwinden
|
||||
continue
|
||||
r = rt.get((self.trip_ids[ti], dstr))
|
||||
r = self.rt_for(rt, ti, dstr)
|
||||
if r and r[0]:
|
||||
continue # ausgefallen
|
||||
arr, dep, skip = self.times(ti, ds, r[1] if r else None)
|
||||
@@ -412,12 +427,13 @@ class Feed:
|
||||
(x1, y1), (x2, y2) = self.point_at(g, dist - 25), self.point_at(g, dist + 25)
|
||||
dx, dy = (x2 - x1) * math.cos(math.radians(lat)), y2 - y1
|
||||
heading = round(math.degrees(math.atan2(dx, dy)), 1) if dx or dy else None
|
||||
route = self.routes[self.trip_route[ti]]
|
||||
route, tc = self.routes[self.trip_route[ti]], self.trip_cat.get(ti)
|
||||
feats.append({
|
||||
"type": "Feature",
|
||||
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
||||
"properties": {
|
||||
"id": f"{dstr}/{ti}", "line": route["line"], "cat": route["cat"], "fv": route["fv"], "sb": route["sb"], "mode": route["mode"],
|
||||
"id": f"{dstr}/{ti}", "line": tc or route["line"], "cat": route["cat"], "mode": route["mode"],
|
||||
"fv": bool(tc) or route["fv"], "sb": not tc and route["sb"],
|
||||
"nr": self.trip_number[ti], "from": self.stop_name[self.st_stop[o]], "to": self.trip_headsign[ti],
|
||||
"delay": delay if r else None, "next": self.stop_name[self.st_stop[o + i]],
|
||||
"heading": heading,
|
||||
@@ -427,10 +443,14 @@ class Feed:
|
||||
})
|
||||
return feats
|
||||
|
||||
def rt_for(self, rt: dict, ti: int, date: str):
|
||||
# Renfe liefert kein Datum; dort ist die Trip-ID je Betriebstag eindeutig
|
||||
return rt.get((self.trip_ids[ti], date)) or rt.get((self.trip_ids[ti], ""))
|
||||
|
||||
def trip_detail(self, date: str, ti: int, now: float, rt: dict):
|
||||
d = dt.datetime.strptime(date, "%Y%m%d").date()
|
||||
ds = day_start(d)
|
||||
r = rt.get((self.trip_ids[ti], date))
|
||||
r = self.rt_for(rt, ti, date)
|
||||
arr, dep, skip = self.times(ti, ds, r[1] if r else None)
|
||||
o0, o1 = self.off[ti], self.off[ti + 1]
|
||||
|
||||
@@ -450,7 +470,7 @@ class Feed:
|
||||
"passed": now - ds > dep[k], "skipped": k in skip,
|
||||
})
|
||||
route = self.routes[self.trip_route[ti]]
|
||||
return {"line": route["line"], "cat": route["cat"], "agency": route["agency"],
|
||||
return {"line": self.trip_cat.get(ti, route["line"]), "cat": route["cat"], "agency": route["agency"],
|
||||
"nr": self.trip_number[ti], "to": self.trip_headsign[ti],
|
||||
"realtime": bool(r), "canceled": bool(r and r[0]) or len(skip) == len(stops),
|
||||
"stops": stops, "shape": self.geometry(ti)[0]}
|
||||
|
||||
Reference in New Issue
Block a user