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_version: str | None = None
|
||||||
rt_at: float | None = None
|
rt_at: float | None = None
|
||||||
rt_error: str | None = None
|
rt_error: str | None = None
|
||||||
|
rt_tried_at: float | None = None
|
||||||
vehicles: bytes = b'{"type":"FeatureCollection","features":[]}'
|
vehicles: bytes = b'{"type":"FeatureCollection","features":[]}'
|
||||||
vehicles_gz: bytes = gzip.compress(vehicles)
|
vehicles_gz: bytes = gzip.compress(vehicles)
|
||||||
count = 0
|
count = 0
|
||||||
@@ -64,6 +65,11 @@ def watch_feeds():
|
|||||||
t = time.time()
|
t = time.time()
|
||||||
S.feed = Feed(C.FEEDS_DIR / want)
|
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))
|
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():
|
def poll_realtime():
|
||||||
@@ -77,6 +83,8 @@ def poll_realtime():
|
|||||||
if S.rt_at and time.time() - S.rt_at > 600:
|
if S.rt_at and time.time() - S.rt_at > 600:
|
||||||
S.rt = {}
|
S.rt = {}
|
||||||
raise
|
raise
|
||||||
|
finally:
|
||||||
|
S.rt_tried_at = time.time()
|
||||||
|
|
||||||
|
|
||||||
def compute_positions():
|
def compute_positions():
|
||||||
@@ -142,6 +150,8 @@ def status():
|
|||||||
"rt_error": S.rt_error,
|
"rt_error": S.rt_error,
|
||||||
"rt_trips": len(S.rt),
|
"rt_trips": len(S.rt),
|
||||||
"vehicles": S.count,
|
"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:
|
// 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)
|
// 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 : {}) } }));
|
layout: { ...l.layout, visibility: "none" }, paint: { ...l.paint, ...(l.type === "symbol" ? extra : {}) } }));
|
||||||
const LAYERS = {
|
const LAYERS = {
|
||||||
light: prefixed(basemaps.layers("osm", basemaps.namedFlavor("light"), { lang: "de" }), "l-"),
|
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 trains = { type: "FeatureCollection", features: [] };
|
||||||
let ts = 0, step = 2, clockOffset = 0;
|
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() {
|
async function refreshTrains() {
|
||||||
if (document.hidden) return;
|
if (document.hidden || !ready) return;
|
||||||
try {
|
try {
|
||||||
const r = await fetch("api/vehicles");
|
const r = await fetch("api/vehicles");
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
@@ -228,25 +230,34 @@ function animate(ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStatus() {
|
async function refreshStatus() {
|
||||||
const el = document.getElementById("status");
|
const el = document.getElementById("status"), loading = document.getElementById("loading");
|
||||||
try {
|
try {
|
||||||
const s = await (await fetch("api/status")).json();
|
const s = await (await fetch("api/status")).json();
|
||||||
if (!s.feed_version) { el.textContent = "Noch kein Fahrplan geladen …"; el.className = "warn"; return; }
|
if (!s.ready) {
|
||||||
const v = s.feed_version;
|
loading.textContent = s.feed_version ? "Züge werden geladen …"
|
||||||
const parts = [`Fahrplan ${v.slice(6, 8)}.${v.slice(4, 6)}.${v.slice(0, 4)}`, `${s.vehicles} Züge`];
|
: "Fahrplan wird vorbereitet … beim ersten Start dauert das 15–20 Minuten.";
|
||||||
let warn = !s.shapes;
|
loading.hidden = false;
|
||||||
if (!s.realtime) { parts.push("keine Echtzeit (API-Key fehlt)"); warn = true; }
|
return;
|
||||||
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 (!ready) {
|
||||||
if (!s.shapes) parts.push("Strecken: Luftlinie");
|
ready = true;
|
||||||
el.textContent = parts.join(" · ");
|
await refreshTrains();
|
||||||
el.className = warn ? "warn" : "";
|
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) {
|
} catch (e) {
|
||||||
el.textContent = "Server nicht erreichbar";
|
loading.textContent = "Keine Verbindung zum Server …";
|
||||||
el.className = "warn";
|
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 ----------
|
// ---------- Zugdetails ----------
|
||||||
|
|
||||||
let openId = null;
|
let openId = null;
|
||||||
@@ -315,10 +326,8 @@ 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);
|
||||||
refreshTrains();
|
statusLoop();
|
||||||
refreshStatus();
|
|
||||||
setInterval(refreshTrains, 5000);
|
setInterval(refreshTrains, 5000);
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
setInterval(refreshStatus, 30000);
|
|
||||||
setInterval(() => openId && showTrip(openId), 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(--d3)"></i>>15′</li>
|
||||||
<li><i style="--c:var(--na)"></i>ohne Echtzeit</li>
|
<li><i style="--c:var(--na)"></i>ohne Echtzeit</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p id="status">Lade …</p>
|
<p id="status"></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div class="card" id="loading" role="status">Züge werden geladen …</div>
|
||||||
|
|
||||||
<aside class="card" id="panel" hidden>
|
<aside class="card" id="panel" hidden>
|
||||||
<button id="close" aria-label="Schliessen">×</button>
|
<button id="close" aria-label="Schliessen">×</button>
|
||||||
<div id="detail"></div>
|
<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-content { background: var(--bg); color: var(--fg); padding: 6px 10px; border-radius: 8px; font-size: 13px; }
|
||||||
.hover .maplibregl-popup-tip { display: none; }
|
.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