Verspätungsfarbe als Verlauf: bis +2′ grün, gelb 10′, orange 20′, rot 30′, lila 40′, ab 50′ blinkend; Legende als Skala

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
Claude
2026-10-22 20:42:13 +02:00
parent be0de5d619
commit 650f8dca8c
3 changed files with 31 additions and 14 deletions
+23 -10
View File
@@ -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) => ({ "&": "&amp;", "<
function delayBadge(sec) {
if (sec === null || sec === undefined) return '<span class="badge" style="background:var(--na)">ohne Echtzeit</span>';
const min = Math.round(sec / 60);
const c = sec < 180 ? "--ok" : sec < 360 ? "--d1" : sec < 900 ? "--d2" : "--d3";
return `<span class="badge" style="background:var(${c})">${min <= 0 ? "pünktlich" : "+" + min + "′"}</span>`;
return `<span class="badge" style="background:${delayRGB(sec)}">${min <= 0 ? "pünktlich" : "+" + min + "′"}</span>`;
}
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 `<span class="d" style="color:var(${c})">${min > 0 ? "+" : ""}${min}′</span>`;
return `<span class="d" style="color:${delayRGB(sec)}">${min > 0 ? "+" : ""}${min}′</span>`;
}
async function showTrip(id) {