diff --git a/tests/test_core.py b/tests/test_core.py index 2034c92..2b256ec 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -110,7 +110,7 @@ def test_all(): assert len(v) == 1 and v[0]["properties"]["delay"] is None lon, lat = v[0]["geometry"]["coordinates"] 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 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 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 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) diff --git a/trainpulse/feed.py b/trainpulse/feed.py index 9c1addf..b2df6f6 100644 --- a/trainpulse/feed.py +++ b/trainpulse/feed.py @@ -5,6 +5,7 @@ import datetime as dt import json import math import mmap +import re from array import array from bisect import bisect_right 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) +# 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: def __init__(self, path: Path, today: dt.date | None = None): self.path = path @@ -79,6 +91,8 @@ class Feed: "cat": r[h["route_desc"]] if "route_desc" 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_headsign, self.trip_number, self.trip_shape = [], [], [] @@ -274,7 +288,7 @@ class Feed: "type": "Feature", "geometry": {"type": "Point", "coordinates": [lon, lat]}, "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], "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 f7eb17a..cb5c8a6 100644 --- a/web/app.js +++ b/web/app.js @@ -108,6 +108,17 @@ function setMode(mode) { } 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 ---------- // 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"; try { mode = localStorage.getItem("mode") || (localStorage.getItem("sat") === "1" ? "sat" : "light"); } catch (e) { /* egal */ } setMode(mode); + let filter = "all"; + try { filter = localStorage.getItem("filter") || "all"; } catch (e) { /* egal */ } + setFilter(filter); statusLoop(); setInterval(refreshTrains, 5000); requestAnimationFrame(animate); diff --git a/web/index.html b/web/index.html index 1830fda..9323168 100644 --- a/web/index.html +++ b/web/index.html @@ -21,6 +21,11 @@ +