From 650f8dca8cfbe281bc21a65bd8f86a5c122537dc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 20:42:13 +0200 Subject: [PATCH] =?UTF-8?q?Versp=C3=A4tungsfarbe=20als=20Verlauf:=20bis=20?= =?UTF-8?q?+2=E2=80=B2=20gr=C3=BCn,=20gelb=2010=E2=80=B2,=20orange=2020?= =?UTF-8?q?=E2=80=B2,=20rot=2030=E2=80=B2,=20lila=2040=E2=80=B2,=20ab=2050?= =?UTF-8?q?=E2=80=B2=20blinkend;=20Legende=20als=20Skala?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb --- web/app.js | 33 +++++++++++++++++++++++---------- web/index.html | 6 ++---- web/style.css | 6 ++++++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/web/app.js b/web/app.js index 513c2b0..4c476b7 100644 --- a/web/app.js +++ b/web/app.js @@ -22,12 +22,24 @@ const LAYERS = { }; // Verspätung in Sekunden -> Farbe (SBB zählt < 3 min als pünktlich) -const delayColor = ["case", - ["==", ["get", "delay"], null], "#94a3b8", - ["<", ["get", "delay"], 180], "#16a34a", - ["<", ["get", "delay"], 360], "#eab308", - ["<", ["get", "delay"], 900], "#f97316", - "#dc2626"]; +// Verspätung (s) -> Farbe, fliessend: bis +2′ grün (wie SBB "pünktlich"), dann über gelb (10′), +// orange (20′), rot (30′) zu lila (40′); ab 50′ blinkt das Symbol zusätzlich (BLINK_S) +const DELAY_STOPS = [[150, "#16a34a"], [600, "#eab308"], [1200, "#f97316"], [1800, "#dc2626"], [2400, "#9333ea"]]; +const NO_RT = "#94a3b8", BLINK_S = 3000; +const delayColor = ["case", ["==", ["get", "delay"], null], NO_RT, + ["interpolate", ["linear"], ["get", "delay"], ...DELAY_STOPS.flat()]]; +function delayRGB(sec) { // dasselbe für die Halteliste (Badges, +x′) + const hex = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16)); + if (sec <= DELAY_STOPS[0][0]) return DELAY_STOPS[0][1]; + for (let i = 1; i < DELAY_STOPS.length; i++) { + const [s0, c0] = DELAY_STOPS[i - 1], [s1, c1] = DELAY_STOPS[i]; + if (sec <= s1) { + const k = (sec - s0) / (s1 - s0), a = hex(c0), b = hex(c1); + return `rgb(${a.map((v, j) => Math.round(v + (b[j] - v) * k)).join(",")})`; + } + } + return DELAY_STOPS.at(-1)[1]; +} const map = new maplibregl.Map({ container: "map", @@ -305,6 +317,9 @@ function render() { f.properties.bearing = cur === undefined ? target : cur + turn(cur, target) * ease; } map.getSource("trains").setData(trains); + // ab 50′ Verspätung sanft ein- und ausblenden (ein Zyklus ~1,6 s) + const pulse = 0.55 + 0.45 * Math.cos((performance.now() / 1600) * 2 * Math.PI); + map.setPaintProperty("trains", "icon-opacity", ["case", [">=", ["coalesce", ["get", "delay"], 0], BLINK_S], pulse, 1]); if (hoverId) { // Hover-Kärtchen fährt mit dem Zug mit const f = trains.features.find((x) => x.properties.id === hoverId); if (f) hover.setLngLat(f.geometry.coordinates); else { hover.remove(); hoverId = null; } @@ -360,15 +375,13 @@ const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "< function delayBadge(sec) { if (sec === null || sec === undefined) return 'ohne Echtzeit'; const min = Math.round(sec / 60); - const c = sec < 180 ? "--ok" : sec < 360 ? "--d1" : sec < 900 ? "--d2" : "--d3"; - return `${min <= 0 ? "pünktlich" : "+" + min + "′"}`; + return `${min <= 0 ? "pünktlich" : "+" + min + "′"}`; } function delayText(sec) { if (sec === null || sec === undefined || Math.round(sec / 60) === 0) return ""; const min = Math.round(sec / 60); - const c = sec < 180 ? "--ok" : sec < 360 ? "--d1" : sec < 900 ? "--d2" : "--d3"; - return `${min > 0 ? "+" : ""}${min}′`; + return `${min > 0 ? "+" : ""}${min}′`; } async function showTrip(id) { diff --git a/web/index.html b/web/index.html index 5b0e1d7..5021f75 100644 --- a/web/index.html +++ b/web/index.html @@ -35,10 +35,8 @@

diff --git a/web/style.css b/web/style.css index 927481c..b40bfe1 100644 --- a/web/style.css +++ b/web/style.css @@ -24,6 +24,12 @@ h1::before { content: ""; display: inline-block; width: 10px; height: 10px; bord .seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); } .legend { list-style: none; padding: 0; margin: 8px 0 0; display: flex; flex-wrap: wrap; gap: 4px 10px; font-size: 12px; color: var(--muted); } +/* Skala 0–50 Minuten, gleiche Stützpunkte wie DELAY_STOPS in app.js */ +.legend .scale { flex: 1 1 160px; } +.legend .bar { display: block; height: 7px; border-radius: 4px; + background: linear-gradient(90deg, #16a34a 5%, #eab308 20%, #f97316 40%, #dc2626 60%, #9333ea 80%, #9333ea); } +.legend .ticks { display: flex; justify-content: space-between; font-size: 11px; } +.legend .ticks b { font-weight: normal; } .legend i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--c); margin-right: 4px; } #search { margin-top: 8px; } #q { width: 100%; box-sizing: border-box; padding: 6px 10px; border: 1px solid var(--line); border-radius: 8px;