Ebenen Regio und S-Bahn getrennt; Lade-Symbol pulsiert bei reduzierter Bewegung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
Claude
2026-10-22 13:35:39 +02:00
parent a7daa76535
commit b5350ae22e
5 changed files with 25 additions and 9 deletions
+3 -1
View File
@@ -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"]
+11 -1
View File
@@ -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,
+5 -4
View File
@@ -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);
+3 -2
View File
@@ -23,8 +23,9 @@
</div>
<div class="seg" role="group" aria-label="Verkehrsmittel">
<button id="flt-all" aria-pressed="true">Alle</button>
<button id="flt-long" aria-pressed="false">Fernverkehr</button>
<button id="flt-local" aria-pressed="false" title="Regionalzüge und S-Bahnen">Nahverkehr</button>
<button id="flt-long" aria-pressed="false" title="Fernverkehr (ICE, IC, TGV …)">Fern</button>
<button id="flt-regio" aria-pressed="false" title="Regionalzüge (RE, RB, TER, Sprinter …)">Regio</button>
<button id="flt-sbahn" aria-pressed="false">S-Bahn</button>
<button id="flt-tram" aria-pressed="false">Tram</button>
<button id="flt-subway" aria-pressed="false">U-Bahn</button>
</div>
+3 -1
View File
@@ -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; } }