Ladeanzeige statt Zwischenständen, Echtzeit sofort nach Start, keine Autobahn-Schilder
- /api/status liefert ready, sobald Positionen mit Echtzeit berechnet sind - Server holt Echtzeit direkt nach dem Laden des Fahrplans (statt nach ~35 s) - Browser zeigt bis dahin nur 'Züge werden geladen …' - Statuszeile ohne Technik: 'N Züge unterwegs', ggf. 'Verspätungen zurzeit nicht verfügbar' - Autobahn-/Strassenschilder (roads_shields) ausgeblendet Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
@@ -29,6 +29,7 @@ class State:
|
||||
rt_version: str | None = None
|
||||
rt_at: float | None = None
|
||||
rt_error: str | None = None
|
||||
rt_tried_at: float | None = None
|
||||
vehicles: bytes = b'{"type":"FeatureCollection","features":[]}'
|
||||
vehicles_gz: bytes = gzip.compress(vehicles)
|
||||
count = 0
|
||||
@@ -64,6 +65,11 @@ def watch_feeds():
|
||||
t = time.time()
|
||||
S.feed = Feed(C.FEEDS_DIR / want)
|
||||
log.info("Fahrplan %s geladen in %.0f s (%d Fahrten)", want, time.time() - t, len(S.feed.trip_ids))
|
||||
try: # Echtzeit sofort holen statt auf den nächsten Abruf zu warten
|
||||
poll_realtime()
|
||||
except Exception:
|
||||
log.exception("Erster Echtzeit-Abruf fehlgeschlagen")
|
||||
compute_positions()
|
||||
|
||||
|
||||
def poll_realtime():
|
||||
@@ -77,6 +83,8 @@ def poll_realtime():
|
||||
if S.rt_at and time.time() - S.rt_at > 600:
|
||||
S.rt = {}
|
||||
raise
|
||||
finally:
|
||||
S.rt_tried_at = time.time()
|
||||
|
||||
|
||||
def compute_positions():
|
||||
@@ -142,6 +150,8 @@ def status():
|
||||
"rt_error": S.rt_error,
|
||||
"rt_trips": len(S.rt),
|
||||
"vehicles": S.count,
|
||||
# erst "bereit", wenn Positionen mit (dem ersten Versuch) Echtzeit berechnet sind
|
||||
"ready": bool(f and S.computed_at and (not C.RT_KEY or (S.rt_tried_at and S.computed_at >= S.rt_tried_at))),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+27
-18
@@ -6,7 +6,7 @@ maplibregl.addProtocol("pmtiles", protocol.tile);
|
||||
|
||||
// Drei Grundkarten-Sätze im selben Style, umgeschaltet per Sichtbarkeit:
|
||||
// hell, dunkel, und Beschriftung für Satellit (weisse Schrift mit dunklem Rand, wie bei Google Maps)
|
||||
const prefixed = (layers, pre, extra = {}) => layers.map((l) => ({ ...l, id: pre + l.id,
|
||||
const prefixed = (layers, pre, extra = {}) => layers.filter((l) => l.id !== "roads_shields").map((l) => ({ ...l, id: pre + l.id,
|
||||
layout: { ...l.layout, visibility: "none" }, paint: { ...l.paint, ...(l.type === "symbol" ? extra : {}) } }));
|
||||
const LAYERS = {
|
||||
light: prefixed(basemaps.layers("osm", basemaps.namedFlavor("light"), { lang: "de" }), "l-"),
|
||||
@@ -114,8 +114,10 @@ for (const m of MODES) document.getElementById("btn-" + m).onclick = () => setMo
|
||||
let trains = { type: "FeatureCollection", features: [] };
|
||||
let ts = 0, step = 2, clockOffset = 0;
|
||||
|
||||
let ready = false; // erst Züge zeigen, wenn der Server fertige Daten (inkl. Echtzeit) hat
|
||||
|
||||
async function refreshTrains() {
|
||||
if (document.hidden) return;
|
||||
if (document.hidden || !ready) return;
|
||||
try {
|
||||
const r = await fetch("api/vehicles");
|
||||
if (!r.ok) return;
|
||||
@@ -228,25 +230,34 @@ function animate(ms) {
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
const el = document.getElementById("status");
|
||||
const el = document.getElementById("status"), loading = document.getElementById("loading");
|
||||
try {
|
||||
const s = await (await fetch("api/status")).json();
|
||||
if (!s.feed_version) { el.textContent = "Noch kein Fahrplan geladen …"; el.className = "warn"; return; }
|
||||
const v = s.feed_version;
|
||||
const parts = [`Fahrplan ${v.slice(6, 8)}.${v.slice(4, 6)}.${v.slice(0, 4)}`, `${s.vehicles} Züge`];
|
||||
let warn = !s.shapes;
|
||||
if (!s.realtime) { parts.push("keine Echtzeit (API-Key fehlt)"); warn = true; }
|
||||
else if (s.rt_error || s.rt_age === null || s.rt_age > 180) { parts.push("Echtzeit gestört"); warn = true; }
|
||||
else parts.push(`Echtzeit vor ${s.rt_age} s`);
|
||||
if (!s.shapes) parts.push("Strecken: Luftlinie");
|
||||
el.textContent = parts.join(" · ");
|
||||
el.className = warn ? "warn" : "";
|
||||
if (!s.ready) {
|
||||
loading.textContent = s.feed_version ? "Züge werden geladen …"
|
||||
: "Fahrplan wird vorbereitet … beim ersten Start dauert das 15–20 Minuten.";
|
||||
loading.hidden = false;
|
||||
return;
|
||||
}
|
||||
if (!ready) {
|
||||
ready = true;
|
||||
await refreshTrains();
|
||||
loading.hidden = true;
|
||||
}
|
||||
const rtOk = s.realtime && !s.rt_error && s.rt_age !== null && s.rt_age < 180;
|
||||
el.textContent = `${s.vehicles} Züge unterwegs` + (rtOk ? "" : " · Verspätungen zurzeit nicht verfügbar");
|
||||
el.className = rtOk ? "" : "warn";
|
||||
} catch (e) {
|
||||
el.textContent = "Server nicht erreichbar";
|
||||
el.className = "warn";
|
||||
loading.textContent = "Keine Verbindung zum Server …";
|
||||
loading.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function statusLoop() { // bis alles bereit ist alle 2 s nachfragen, danach alle 30 s
|
||||
await refreshStatus();
|
||||
setTimeout(statusLoop, ready ? 30000 : 2000);
|
||||
}
|
||||
|
||||
// ---------- Zugdetails ----------
|
||||
|
||||
let openId = null;
|
||||
@@ -315,10 +326,8 @@ map.on("load", () => {
|
||||
let mode = "light";
|
||||
try { mode = localStorage.getItem("mode") || (localStorage.getItem("sat") === "1" ? "sat" : "light"); } catch (e) { /* egal */ }
|
||||
setMode(mode);
|
||||
refreshTrains();
|
||||
refreshStatus();
|
||||
statusLoop();
|
||||
setInterval(refreshTrains, 5000);
|
||||
requestAnimationFrame(animate);
|
||||
setInterval(refreshStatus, 30000);
|
||||
setInterval(() => openId && showTrip(openId), 30000);
|
||||
});
|
||||
|
||||
+3
-1
@@ -28,9 +28,11 @@
|
||||
<li><i style="--c:var(--d3)"></i>>15′</li>
|
||||
<li><i style="--c:var(--na)"></i>ohne Echtzeit</li>
|
||||
</ul>
|
||||
<p id="status">Lade …</p>
|
||||
<p id="status"></p>
|
||||
</header>
|
||||
|
||||
<div class="card" id="loading" role="status">Züge werden geladen …</div>
|
||||
|
||||
<aside class="card" id="panel" hidden>
|
||||
<button id="close" aria-label="Schliessen">×</button>
|
||||
<div id="detail"></div>
|
||||
|
||||
@@ -46,3 +46,11 @@ h1::before { content: ""; display: inline-block; width: 10px; height: 10px; bord
|
||||
|
||||
.hover .maplibregl-popup-content { background: var(--bg); color: var(--fg); padding: 6px 10px; border-radius: 8px; font-size: 13px; }
|
||||
.hover .maplibregl-popup-tip { display: none; }
|
||||
|
||||
#loading { left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 14px 20px; font-size: 15px;
|
||||
max-width: calc(100vw - 48px); text-align: center; z-index: 3; }
|
||||
#loading[hidden] { display: none; }
|
||||
#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; } }
|
||||
|
||||
Reference in New Issue
Block a user