From a7daa7653552e4055a07020e6f5981c747214935 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 13:30:42 +0200 Subject: [PATCH] =?UTF-8?q?Ebenen=20statt=20Filter=20(Fern,=20Nah,=20Tram,?= =?UTF-8?q?=20U-Bahn=20einzeln=20oder=20alle);=20Haltename=20beim=20Dar?= =?UTF-8?q?=C3=BCberfahren;=20Lade-Hinweis=20nach=20Server-Neustart?= 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 | 64 +++++++++++++++++++++++++++++++++++--------------- web/index.html | 7 +++--- web/style.css | 6 ++--- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/web/app.js b/web/app.js index 047cbda..9b8130c 100644 --- a/web/app.js +++ b/web/app.js @@ -122,18 +122,34 @@ function setMode(mode) { } for (const m of MODES) document.getElementById("btn-" + m).onclick = () => setMode(m); -// ---------- Filter: alle / Nahverkehr / Fernverkehr / Tram & U-Bahn ---------- +// ---------- Ebenen: Fernverkehr / Nahverkehr / Tram / U-Bahn, einzeln oder alle ---------- -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)); +const LAYER_FILTERS = { + local: ["all", ["==", ["get", "mode"], "rail"], ["!", ["get", "fv"]]], long: ["get", "fv"], + tram: ["==", ["get", "mode"], "tram"], subway: ["==", ["get", "mode"], "subway"], +}; +const ALL_LAYERS = Object.keys(LAYER_FILTERS); +const layerOf = (p) => (p.mode === "rail" ? (p.fv ? "long" : "local") : p.mode); +let shown = new Set(ALL_LAYERS); +function setLayers(list) { + shown = new Set(list.filter((k) => k in LAYER_FILTERS)); + if (!shown.size) shown = new Set(ALL_LAYERS); // nichts gewählt = alles + const all = shown.size === ALL_LAYERS.length; + const filter = all ? null : ["any", ...[...shown].map((k) => LAYER_FILTERS[k])]; + for (const id of ["trains", "train-labels"]) map.setFilter(id, filter); + document.getElementById("flt-all").setAttribute("aria-pressed", String(all)); + for (const k of ALL_LAYERS) document.getElementById("flt-" + k).setAttribute("aria-pressed", String(!all && shown.has(k))); updateHint(); - try { localStorage.setItem("filter", name); } catch (e) { /* privater Modus */ } + try { localStorage.setItem("layers", JSON.stringify([...shown])); } catch (e) { /* privater Modus */ } } -for (const n of Object.keys(FILTERS)) document.getElementById("flt-" + n).onclick = () => setFilter(n); +document.getElementById("flt-all").onclick = () => setLayers(ALL_LAYERS); +for (const k of ALL_LAYERS) document.getElementById("flt-" + k).onclick = () => { + // aus "Alle" heraus: nur diese Ebene; sonst Ebene dazu oder weg + if (shown.size === ALL_LAYERS.length) return setLayers([k]); + const next = new Set(shown); + next.has(k) ? next.delete(k) : next.add(k); + setLayers([...next]); +}; // ---------- Züge ---------- @@ -156,8 +172,9 @@ async function getJSON(url) { } function updateHint() { - const city = document.getElementById("flt-city").getAttribute("aria-pressed") === "true"; - document.getElementById("hint").hidden = !(city && map.getZoom() < URBAN_Z); + // nur Trams/U-Bahnen gewählt, aber zu weit weg: sonst wäre die Karte einfach leer + const onlyCity = !shown.has("long") && !shown.has("local"); + document.getElementById("hint").hidden = !(onlyCity && map.getZoom() < URBAN_Z); } map.on("zoomend", updateHint); // beim Verschieben/Zoomen Trams nachladen, wenn der Ausschnitt den zuletzt geladenen verlässt @@ -306,7 +323,9 @@ async function refreshStatus() { const el = document.getElementById("status"), loading = document.getElementById("loading"); try { const s = await (await fetch("api/status")).json(); - if (!s.ready && !ready) { // nur beim Start; danach meldet der Server nach jedem Echtzeit-Abruf kurz "nicht bereit" + // beim Start oder nach Server-Neustart (noch keine Züge); sonst meldet der Server nach jedem + // Echtzeit-Abruf nur kurz "nicht bereit" + if (!s.ready && (!ready || !s.vehicles)) { loading.textContent = s.loaded ? "Züge werden geladen …" : "Fahrplan wird vorbereitet … beim ersten Start dauert das 1–1,5 Stunden."; loading.hidden = false; @@ -329,7 +348,7 @@ async function refreshStatus() { async function statusLoop() { // bis alles bereit ist alle 2 s nachfragen, danach alle 30 s await refreshStatus(); - setTimeout(statusLoop, ready ? 30000 : 2000); + setTimeout(statusLoop, ready && document.getElementById("loading").hidden ? 30000 : 2000); } // ---------- Zugdetails ---------- @@ -371,7 +390,7 @@ async function showTrip(id) { document.getElementById("panel").hidden = false; map.getSource("route").setData({ type: "Feature", geometry: { type: "LineString", coordinates: t.shape } }); map.getSource("route-stops").setData({ type: "FeatureCollection", features: t.stops.map((x) => ( - { type: "Feature", properties: {}, geometry: { type: "Point", coordinates: [x.lon, x.lat] } })) }); + { type: "Feature", properties: { name: x.name }, geometry: { type: "Point", coordinates: [x.lon, x.lat] } })) }); } // ---------- Suche ---------- @@ -402,8 +421,7 @@ function pick(i) { const f = hits[i]; if (!f) return; const now = trains.features.find((x) => x.properties.id === f.properties.id) || f; // aktuelle Position - const pressed = Object.keys(FILTERS).find((n) => document.getElementById("flt-" + n).getAttribute("aria-pressed") === "true"); - if (pressed === "local" && f.properties.fv || pressed === "long" && !f.properties.fv) setFilter("all"); // sonst unsichtbar + if (!shown.has(layerOf(f.properties))) setLayers([...shown, layerOf(f.properties)]); // sonst unsichtbar map.flyTo({ center: now.geometry.coordinates, zoom: Math.max(map.getZoom(), 12) }); showTrip(f.properties.id); document.getElementById("results").hidden = true; @@ -453,13 +471,21 @@ map.on("mousemove", "trains", (e) => { }); map.on("mouseleave", "trains", () => { map.getCanvas().style.cursor = ""; hover.remove(); hoverId = null; }); +// Halte der geöffneten Fahrt: Name beim Darüberfahren +const stopHover = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 8, className: "hover" }); +map.on("mousemove", "route-stops", (e) => { + if (hoverId) return; // Zug-Kärtchen hat Vorrang + stopHover.setText(e.features[0].properties.name).setLngLat(e.features[0].geometry.coordinates).addTo(map); +}); +map.on("mouseleave", "route-stops", () => stopHover.remove()); + map.on("load", () => { let mode = "light"; try { mode = localStorage.getItem("mode") || (localStorage.getItem("sat") === "1" ? "sat" : "light"); } catch (e) { /* egal */ } setMode(mode); - let filter = "all"; - try { filter = localStorage.getItem("filter") || "all"; } catch (e) { /* egal */ } - setFilter(filter); + let layers = ALL_LAYERS; + try { layers = JSON.parse(localStorage.getItem("layers")) || ALL_LAYERS; } catch (e) { /* egal */ } + setLayers(Array.isArray(layers) ? layers : ALL_LAYERS); statusLoop(); setInterval(refreshTrains, 5000); requestAnimationFrame(animate); diff --git a/web/index.html b/web/index.html index 5eba271..89a06ad 100644 --- a/web/index.html +++ b/web/index.html @@ -21,11 +21,12 @@ -
+
- - + + +