diff --git a/tests/test_core.py b/tests/test_core.py index eb977d0..462c516 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -196,7 +196,9 @@ def test_all(): assert len(v) == 1 and v[0]["properties"]["to"] == "Gamma" and v[0]["properties"]["nr"] == "" assert de.vehicles(dt.datetime(2026, 10, 25, 8, 5, tzinfo=TZ).timestamp(), {}) == [] # calendar_dates - from trainpulse.feed import STOP_SUFFIX, long_distance, route_mode + from trainpulse.feed import STOP_SUFFIX, long_distance, route_mode, sbahn + assert all(sbahn(c, l) for c, l in (("S", "S5"), ("SN", "SN7"), ("", "S1"), ("", "S 5"), ("", "S61"), ("", "S"))) + assert not any(sbahn(c, l) for c, l in (("", "RE1"), ("", "RB26"), ("TER", "K3"), ("", "SPR"), ("", "SE10"), ("", "STB"))) assert [STOP_SUFFIX.sub("", n) for n in ("S+U Rathaus Spandau Bstggl.2", "Zingster Str., Ausstieg Gl.3-5", "Virchow-Klinikum, Ausstieg", "Zürich HB", "Bahnhof, Oranienburg")] == \ ["S+U Rathaus Spandau", "Zingster Str.", "Virchow-Klinikum", "Zürich HB", "Bahnhof, Oranienburg"] diff --git a/trainpulse/feed.py b/trainpulse/feed.py index 3c0543e..2fe8e8a 100644 --- a/trainpulse/feed.py +++ b/trainpulse/feed.py @@ -84,6 +84,15 @@ def route_mode(route_type: str) -> str | None: return 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 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 "") @@ -180,6 +189,7 @@ class Feed: 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"]) # Haltezeiten flach in Arrays; off[i]..off[i+1] sind die Halte von Fahrt i n = len(self.trip_route) @@ -407,7 +417,7 @@ class Feed: "type": "Feature", "geometry": {"type": "Point", "coordinates": [lon, lat]}, "properties": { - "id": f"{dstr}/{ti}", "line": route["line"], "cat": route["cat"], "fv": route["fv"], "mode": route["mode"], + "id": f"{dstr}/{ti}", "line": route["line"], "cat": route["cat"], "fv": route["fv"], "sb": route["sb"], "mode": route["mode"], "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, diff --git a/web/app.js b/web/app.js index 9b8130c..513c2b0 100644 --- a/web/app.js +++ b/web/app.js @@ -122,14 +122,15 @@ function setMode(mode) { } for (const m of MODES) document.getElementById("btn-" + m).onclick = () => setMode(m); -// ---------- Ebenen: Fernverkehr / Nahverkehr / Tram / U-Bahn, einzeln oder alle ---------- +// ---------- Ebenen: Fernverkehr / Regionalzug / S-Bahn / Tram / U-Bahn, einzeln oder alle ---------- const LAYER_FILTERS = { - local: ["all", ["==", ["get", "mode"], "rail"], ["!", ["get", "fv"]]], long: ["get", "fv"], + long: ["get", "fv"], regio: ["all", ["==", ["get", "mode"], "rail"], ["!", ["get", "fv"]], ["!", ["get", "sb"]]], + sbahn: ["get", "sb"], tram: ["==", ["get", "mode"], "tram"], subway: ["==", ["get", "mode"], "subway"], }; const ALL_LAYERS = Object.keys(LAYER_FILTERS); -const layerOf = (p) => (p.mode === "rail" ? (p.fv ? "long" : "local") : p.mode); +const layerOf = (p) => (p.mode === "rail" ? (p.fv ? "long" : p.sb ? "sbahn" : "regio") : p.mode); let shown = new Set(ALL_LAYERS); function setLayers(list) { shown = new Set(list.filter((k) => k in LAYER_FILTERS)); @@ -173,7 +174,7 @@ async function getJSON(url) { function updateHint() { // nur Trams/U-Bahnen gewählt, aber zu weit weg: sonst wäre die Karte einfach leer - const onlyCity = !shown.has("long") && !shown.has("local"); + const onlyCity = !["long", "regio", "sbahn"].some((k) => shown.has(k)); document.getElementById("hint").hidden = !(onlyCity && map.getZoom() < URBAN_Z); } map.on("zoomend", updateHint); diff --git a/web/index.html b/web/index.html index 89a06ad..5b0e1d7 100644 --- a/web/index.html +++ b/web/index.html @@ -23,8 +23,9 @@
- - + + +
diff --git a/web/style.css b/web/style.css index 706f2a1..24d389a 100644 --- a/web/style.css +++ b/web/style.css @@ -65,4 +65,6 @@ h1::before { content: ""; display: inline-block; width: 10px; height: 10px; bord #loading::before { content: ""; display: inline-block; width: 14px; height: 14px; margin-right: 10px; vertical-align: -2px; border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } -@media (prefers-reduced-motion: reduce) { #loading::before { animation: none; } } +/* System will weniger Bewegung (Windows "Animationseffekte" aus): Pulsieren statt Drehen */ +@media (prefers-reduced-motion: reduce) { #loading::before { animation: pulse 1.6s ease-in-out infinite; } } +@keyframes pulse { 50% { opacity: .3; } }