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:
+6
-1
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
Reference in New Issue
Block a user