Ebenen statt Filter (Fern, Nah, Tram, U-Bahn einzeln oder alle); Haltename beim Darüberfahren; Lade-Hinweis nach Server-Neustart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
+45
-19
@@ -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);
|
||||
|
||||
+4
-3
@@ -21,11 +21,12 @@
|
||||
<button id="btn-dark" aria-pressed="false">Dunkel</button>
|
||||
<button id="btn-sat" aria-pressed="false">Satellit</button>
|
||||
</div>
|
||||
<div class="seg" role="group" aria-label="Züge">
|
||||
<div class="seg" role="group" aria-label="Verkehrsmittel">
|
||||
<button id="flt-all" aria-pressed="true">Alle</button>
|
||||
<button id="flt-local" aria-pressed="false">Nahverkehr</button>
|
||||
<button id="flt-long" aria-pressed="false">Fernverkehr</button>
|
||||
<button id="flt-city" aria-pressed="false" title="Trams und U-Bahnen">Tram/U</button>
|
||||
<button id="flt-local" aria-pressed="false" title="Regionalzüge und S-Bahnen">Nahverkehr</button>
|
||||
<button id="flt-tram" aria-pressed="false">Tram</button>
|
||||
<button id="flt-subway" aria-pressed="false">U-Bahn</button>
|
||||
</div>
|
||||
<div id="search">
|
||||
<input id="q" type="search" placeholder="Zug suchen, z. B. S5, ICE 11, 2318" autocomplete="off"
|
||||
|
||||
+3
-3
@@ -14,13 +14,13 @@ html, body { margin: 0; height: 100%; }
|
||||
position: absolute; z-index: 2; background: var(--bg); border-radius: 10px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,.18);
|
||||
}
|
||||
#bar { top: 12px; left: 12px; padding: 10px 12px; max-width: min(340px, calc(100vw - 24px)); box-sizing: border-box; }
|
||||
#bar { top: 12px; left: 12px; padding: 10px 12px; max-width: min(380px, calc(100vw - 24px)); box-sizing: border-box; }
|
||||
h1 { margin: 0 0 8px; font-size: 16px; letter-spacing: .02em; }
|
||||
h1::before { content: ""; display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: var(--accent); margin-right: 7px; }
|
||||
|
||||
.seg + .seg { margin-top: 6px; }
|
||||
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
||||
.seg button { border: 0; background: transparent; color: var(--fg); padding: 6px 11px; font: inherit; cursor: pointer; }
|
||||
.seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
||||
.seg button { border: 0; background: transparent; color: var(--fg); padding: 6px 8px; font: inherit; cursor: pointer; }
|
||||
.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); }
|
||||
|
||||
Reference in New Issue
Block a user