Trams und U-Bahnen (CH, DE, NL): eigener Filter, ab Stadtzoom im Ausschnitt; NL-Echtzeit auch für Tram/Metro; Vorausschau ohne 7-fache Neuberechnung; deutsche Steig-Zusätze aus Haltestellennamen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
+47
-15
@@ -77,7 +77,10 @@ const map = new maplibregl.Map({
|
||||
"icon-image": ["case", ["has", "bearing"], "tp-arrow", "tp-dot"],
|
||||
"icon-rotate": ["coalesce", ["get", "bearing"], 0],
|
||||
"icon-rotation-alignment": "map",
|
||||
"icon-size": ["interpolate", ["linear"], ["zoom"], 6, 0.3, 9, 0.5, 12, 0.9, 14, 1.3],
|
||||
// Trams/U-Bahnen etwas kleiner als Züge
|
||||
// (zoom darf nur zuoberst in interpolate stehen, darum der Faktor in jedem Stützwert)
|
||||
"icon-size": ["interpolate", ["linear"], ["zoom"],
|
||||
...[[6, 0.3], [9, 0.5], [12, 0.9], [14, 1.3]].flatMap(([z, v]) => [z, ["*", v, ["case", ["==", ["get", "mode"], "rail"], 1, 0.75]]])],
|
||||
"icon-allow-overlap": true, "icon-ignore-placement": true,
|
||||
},
|
||||
paint: {
|
||||
@@ -119,13 +122,15 @@ function setMode(mode) {
|
||||
}
|
||||
for (const m of MODES) document.getElementById("btn-" + m).onclick = () => setMode(m);
|
||||
|
||||
// ---------- Filter: alle / Nahverkehr / Fernverkehr ----------
|
||||
// ---------- Filter: alle / Nahverkehr / Fernverkehr / Tram & U-Bahn ----------
|
||||
|
||||
const FILTERS = { all: null, local: ["!", ["get", "fv"]], long: ["get", "fv"] };
|
||||
const RAIL = ["==", ["get", "mode"], "rail"];
|
||||
const FILTERS = { all: null, local: ["all", RAIL, ["!", ["get", "fv"]]], long: ["get", "fv"], city: ["!", RAIL] };
|
||||
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));
|
||||
updateHint();
|
||||
try { localStorage.setItem("filter", name); } catch (e) { /* privater Modus */ }
|
||||
}
|
||||
for (const n of Object.keys(FILTERS)) document.getElementById("flt-" + n).onclick = () => setFilter(n);
|
||||
@@ -135,20 +140,48 @@ for (const n of Object.keys(FILTERS)) document.getElementById("flt-" + n).onclic
|
||||
// Server liefert je Zug die Gleispunkte der nächsten ~10 s, jeweils mit der Zeit, zu der der Zug dort ist;
|
||||
// der Browser fährt sie ab, die Spitze dreht sich weich in die Richtung des aktuellen Gleisstücks.
|
||||
let trains = { type: "FeatureCollection", features: [] };
|
||||
let ts = 0, clockOffset = 0;
|
||||
let clockOffset = 0;
|
||||
const URBAN_Z = 11; // Trams/U-Bahnen erst ab hier (vorher nur Gewimmel, und sonst zu viele Daten)
|
||||
|
||||
let ready = false; // erst Züge zeigen, wenn der Server fertige Daten (inkl. Echtzeit) hat
|
||||
|
||||
async function getJSON(url) {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(r.status);
|
||||
const now = Number(r.headers.get("X-Now"));
|
||||
if (now) clockOffset = Date.now() / 1000 - now;
|
||||
const d = await r.json();
|
||||
for (const f of d.features) f.t0 = d.ts; // Gleispunkt-Zeiten zählen ab der Berechnung auf dem Server
|
||||
return d.features;
|
||||
}
|
||||
|
||||
function updateHint() {
|
||||
const city = document.getElementById("flt-city").getAttribute("aria-pressed") === "true";
|
||||
document.getElementById("hint").hidden = !(city && map.getZoom() < URBAN_Z);
|
||||
}
|
||||
map.on("zoomend", updateHint);
|
||||
// beim Verschieben/Zoomen Trams nachladen, wenn der Ausschnitt den zuletzt geladenen verlässt
|
||||
map.on("moveend", () => {
|
||||
if (!ready) return;
|
||||
const b = map.getBounds(), want = map.getZoom() >= URBAN_Z;
|
||||
const inside = urbanBox && b.getWest() >= urbanBox[0] && b.getSouth() >= urbanBox[1]
|
||||
&& b.getEast() <= urbanBox[2] && b.getNorth() <= urbanBox[3];
|
||||
if (want ? !inside : urbanBox) refreshTrains();
|
||||
});
|
||||
|
||||
let urbanBox = null; // zuletzt abgefragter Ausschnitt (mit Rand), um beim Verschieben nachzuladen
|
||||
async function refreshTrains() {
|
||||
if (document.hidden || !ready) return;
|
||||
try {
|
||||
const r = await fetch("api/vehicles");
|
||||
if (!r.ok) return;
|
||||
const now = Number(r.headers.get("X-Now"));
|
||||
const d = await r.json();
|
||||
if (now) clockOffset = Date.now() / 1000 - now;
|
||||
let urban = [];
|
||||
if (map.getZoom() >= URBAN_Z) {
|
||||
const b = map.getBounds(), dx = (b.getEast() - b.getWest()) * 0.3, dy = (b.getNorth() - b.getSouth()) * 0.3;
|
||||
urbanBox = [b.getWest() - dx, b.getSouth() - dy, b.getEast() + dx, b.getNorth() + dy];
|
||||
urban = await getJSON("api/urban?bbox=" + urbanBox.map((v) => v.toFixed(4)).join(","));
|
||||
} else urbanBox = null;
|
||||
const features = [...(await getJSON("api/vehicles")), ...urban];
|
||||
const old = new Map(trains.features.map((f) => [f.properties.id, f]));
|
||||
for (const f of d.features) {
|
||||
for (const f of features) {
|
||||
f.pts = f.path && f.path.length ? f.path : [[...f.geometry.coordinates, 0]]; // [lon, lat, s ab ts, m ab Fahrtbeginn]
|
||||
const o = old.get(f.properties.id);
|
||||
if (o) {
|
||||
@@ -157,8 +190,7 @@ async function refreshTrains() {
|
||||
f.shownD = o.shownD;
|
||||
}
|
||||
}
|
||||
trains = d;
|
||||
ts = d.ts;
|
||||
trains = { type: "FeatureCollection", features };
|
||||
render(); // sofort auf die aktuelle Position, nicht erst beim nächsten Bild
|
||||
} catch (e) { /* nächster Versuch in ein paar Sekunden */ }
|
||||
}
|
||||
@@ -230,12 +262,12 @@ map.addImage("tp-dot", sdfIcon(shapeMask((ctx) => {
|
||||
let lastRender = 0;
|
||||
const MAX_HOLD_M = 5000; // ~1 min eines schnellen Zuges
|
||||
function render() {
|
||||
const t = Date.now() / 1000 - clockOffset - ts; // s seit Berechnung auf dem Server
|
||||
const now = Date.now() / 1000 - clockOffset;
|
||||
const dtFrame = Math.min(0.2, (performance.now() - lastRender) / 1000); // nach Ruckler nicht schlagartig drehen
|
||||
lastRender = performance.now();
|
||||
const ease = 1 - Math.exp(-dtFrame * 6); // Richtung folgt in ~0,3 s
|
||||
for (const f of trains.features) {
|
||||
const p = f.pts;
|
||||
const p = f.pts, t = now - f.t0; // s seit Berechnung auf dem Server
|
||||
if (!p) continue;
|
||||
// Gleispunkte mit Zeitstempel: Abschnitt suchen, in dem der Zug gerade ist, und darauf fahren
|
||||
let i = 0;
|
||||
@@ -286,7 +318,7 @@ async function refreshStatus() {
|
||||
}
|
||||
loading.hidden = true; // auch nach "Keine Verbindung" wieder weg
|
||||
const rtOk = s.rt_ok;
|
||||
el.textContent = `${s.vehicles} Züge unterwegs` + (rtOk ? "" : s.rt_some
|
||||
el.textContent = `${s.vehicles} Züge` + (s.urban ? `, ${s.urban} Trams/U-Bahnen` : "") + " unterwegs" + (rtOk ? "" : s.rt_some
|
||||
? " · Verspätungen teilweise nicht verfügbar" : " · Verspätungen zurzeit nicht verfügbar");
|
||||
el.className = rtOk ? "" : "warn";
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user