Filter: alle / Nahverkehr / Fernverkehr

Einteilung nach Gattung (Schweiz: route_desc, Deutschland: Präfix der
Linie): ICE, IC, EC, ECE, EN, NJ, RJ(X), TGV, IR, … = Fernverkehr.
Auswahl bleibt im Browser gespeichert.

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 06:01:38 +02:00
parent 085a129a85
commit 8865e7abcf
5 changed files with 41 additions and 2 deletions
+6 -1
View File
@@ -110,7 +110,7 @@ def test_all():
assert len(v) == 1 and v[0]["properties"]["delay"] is None assert len(v) == 1 and v[0]["properties"]["delay"] is None
lon, lat = v[0]["geometry"]["coordinates"] lon, lat = v[0]["geometry"]["coordinates"]
assert lat < 47.0 and abs(lon - 8.05) < 0.01, (lon, lat) # halbe Fahrzeit = Umweg-Scheitelpunkt assert lat < 47.0 and abs(lon - 8.05) < 0.01, (lon, lat) # halbe Fahrzeit = Umweg-Scheitelpunkt
assert v[0]["properties"]["next"] == "Beta" assert v[0]["properties"]["next"] == "Beta" and v[0]["properties"]["fv"] is True # IC = Fernverkehr
assert abs(v[0]["properties"]["heading"] - 90) < 15, v[0]["properties"]["heading"] # Scheitel: Richtung Osten assert abs(v[0]["properties"]["heading"] - 90) < 15, v[0]["properties"]["heading"] # Scheitel: Richtung Osten
assert f.vehicles(at(8, 11), {})[0]["properties"]["heading"] is not None # auch im Stillstand assert f.vehicles(at(8, 11), {})[0]["properties"]["heading"] is not None # auch im Stillstand
@@ -150,6 +150,11 @@ def test_all():
assert len(v) == 1 and v[0]["properties"]["to"] == "Gamma" and v[0]["properties"]["nr"] == "" 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 assert de.vehicles(dt.datetime(2026, 10, 25, 8, 5, tzinfo=TZ).timestamp(), {}) == [] # calendar_dates
from trainpulse.feed import long_distance
assert long_distance("", "ICE 75") and long_distance("IR", "IR36") and long_distance("", "ECE 20")
assert not long_distance("S", "S5") and not long_distance("", "RE6") and not long_distance("", "IRE 3")
assert not long_distance("TER", "K1") and not long_distance("", "")
from trainpulse.border import SHOW, in_switzerland from trainpulse.border import SHOW, in_switzerland
assert in_switzerland(8.540, 47.378) and not in_switzerland(9.177, 47.659) # Zürich HB / Konstanz assert in_switzerland(8.540, 47.378) and not in_switzerland(9.177, 47.659) # Zürich HB / Konstanz
assert not in_switzerland(8.690, 47.697) # Büsingen (deutsche Exklave) assert not in_switzerland(8.690, 47.697) # Büsingen (deutsche Exklave)
+15 -1
View File
@@ -5,6 +5,7 @@ import datetime as dt
import json import json
import math import math
import mmap import mmap
import re
from array import array from array import array
from bisect import bisect_right from bisect import bisect_right
from pathlib import Path from pathlib import Path
@@ -57,6 +58,17 @@ def travelled(length: float, duration: float, t: float) -> float:
return 0.5 * a * ta * ta + vmax * (t - ta) return 0.5 * a * ta * ta + vmax * (t - ta)
# Gattungen des Fernverkehrs; alles andere (RE, RB, S, R, TER, …) gilt als Nahverkehr
LONG_DISTANCE = {"ICE", "IC", "ICN", "EC", "ECE", "ECM", "EN", "NJ", "RJ", "RJX", "TGV", "THA", "EST", "FLX",
"IR", "PE", "D"}
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
class Feed: class Feed:
def __init__(self, path: Path, today: dt.date | None = None): def __init__(self, path: Path, today: dt.date | None = None):
self.path = path self.path = path
@@ -79,6 +91,8 @@ class Feed:
"cat": r[h["route_desc"]] if "route_desc" in h else "", "cat": r[h["route_desc"]] if "route_desc" in h else "",
"agency": agencies.get(r[h["agency_id"]], "") if "agency_id" in h else "", "agency": agencies.get(r[h["agency_id"]], "") if "agency_id" in h else "",
} }
route = self.routes[r[h["route_id"]]]
route["fv"] = long_distance(route["cat"], route["line"])
self.trip_index, self.trip_ids, self.trip_route, self.trip_service = {}, [], [], [] self.trip_index, self.trip_ids, self.trip_route, self.trip_service = {}, [], [], []
self.trip_headsign, self.trip_number, self.trip_shape = [], [], [] self.trip_headsign, self.trip_number, self.trip_shape = [], [], []
@@ -274,7 +288,7 @@ class Feed:
"type": "Feature", "type": "Feature",
"geometry": {"type": "Point", "coordinates": [lon, lat]}, "geometry": {"type": "Point", "coordinates": [lon, lat]},
"properties": { "properties": {
"id": f"{dstr}/{ti}", "line": route["line"], "cat": route["cat"], "id": f"{dstr}/{ti}", "line": route["line"], "cat": route["cat"], "fv": route["fv"],
"nr": self.trip_number[ti], "to": self.trip_headsign[ti], "nr": self.trip_number[ti], "to": self.trip_headsign[ti],
"delay": delay if r else None, "next": self.stop_name[self.st_stop[o + i]], "delay": delay if r else None, "next": self.stop_name[self.st_stop[o + i]],
"heading": heading, "heading": heading,
+14
View File
@@ -108,6 +108,17 @@ function setMode(mode) {
} }
for (const m of MODES) document.getElementById("btn-" + m).onclick = () => setMode(m); for (const m of MODES) document.getElementById("btn-" + m).onclick = () => setMode(m);
// ---------- Filter: alle / Nahverkehr / Fernverkehr ----------
const FILTERS = { all: null, local: ["!", ["get", "fv"]], long: ["get", "fv"] };
function setFilter(name) {
if (!(name in FILTERS)) name = "all";
for (const id of ["trains", "train-labels"]) map.setFilter(id, FILTERS[name]);
for (const n of Object.keys(FILTERS)) document.getElementById("flt-" + n).setAttribute("aria-pressed", String(n === name));
try { localStorage.setItem("filter", name); } catch (e) { /* privater Modus */ }
}
for (const n of Object.keys(FILTERS)) document.getElementById("flt-" + n).onclick = () => setFilter(n);
// ---------- Züge ---------- // ---------- Züge ----------
// Server liefert je Zug die Position jetzt und alle `step` Sekunden danach (entlang der Strecke); // Server liefert je Zug die Position jetzt und alle `step` Sekunden danach (entlang der Strecke);
@@ -328,6 +339,9 @@ map.on("load", () => {
let mode = "light"; let mode = "light";
try { mode = localStorage.getItem("mode") || (localStorage.getItem("sat") === "1" ? "sat" : "light"); } catch (e) { /* egal */ } try { mode = localStorage.getItem("mode") || (localStorage.getItem("sat") === "1" ? "sat" : "light"); } catch (e) { /* egal */ }
setMode(mode); setMode(mode);
let filter = "all";
try { filter = localStorage.getItem("filter") || "all"; } catch (e) { /* egal */ }
setFilter(filter);
statusLoop(); statusLoop();
setInterval(refreshTrains, 5000); setInterval(refreshTrains, 5000);
requestAnimationFrame(animate); requestAnimationFrame(animate);
+5
View File
@@ -21,6 +21,11 @@
<button id="btn-dark" aria-pressed="false">Dunkel</button> <button id="btn-dark" aria-pressed="false">Dunkel</button>
<button id="btn-sat" aria-pressed="false">Satellit</button> <button id="btn-sat" aria-pressed="false">Satellit</button>
</div> </div>
<div class="seg" role="group" aria-label="Züge">
<button id="flt-all" aria-pressed="true">Alle</button>
<button id="flt-local" aria-pressed="false">Nahverkehr</button>
<button id="flt-long" aria-pressed="false">Fernverkehr</button>
</div>
<ul class="legend"> <ul class="legend">
<li><i style="--c:var(--ok)"></i>pünktlich</li> <li><i style="--c:var(--ok)"></i>pünktlich</li>
<li><i style="--c:var(--d1)"></i>3–6′</li> <li><i style="--c:var(--d1)"></i>3–6′</li>
+1
View File
@@ -18,6 +18,7 @@ html, body { margin: 0; height: 100%; }
h1 { margin: 0 0 8px; font-size: 16px; letter-spacing: .02em; } h1 { margin: 0 0 8px; font-size: 16px; letter-spacing: .02em; }
h1::before { content: ""; display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--accent); margin-right: 7px; } h1::before { content: ""; display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--accent); margin-right: 7px; }
.seg + .seg { margin-top: 6px; }
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; } .seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.seg button { border: 0; background: transparent; color: var(--fg); padding: 6px 14px; font: inherit; cursor: pointer; } .seg button { border: 0; background: transparent; color: var(--fg); padding: 6px 14px; font: inherit; cursor: pointer; }
.seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); } .seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }