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);
|
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 LAYER_FILTERS = {
|
||||||
const FILTERS = { all: null, local: ["all", RAIL, ["!", ["get", "fv"]]], long: ["get", "fv"], city: ["!", RAIL] };
|
local: ["all", ["==", ["get", "mode"], "rail"], ["!", ["get", "fv"]]], long: ["get", "fv"],
|
||||||
function setFilter(name) {
|
tram: ["==", ["get", "mode"], "tram"], subway: ["==", ["get", "mode"], "subway"],
|
||||||
if (!(name in FILTERS)) name = "all";
|
};
|
||||||
for (const id of ["trains", "train-labels"]) map.setFilter(id, FILTERS[name]);
|
const ALL_LAYERS = Object.keys(LAYER_FILTERS);
|
||||||
for (const n of Object.keys(FILTERS)) document.getElementById("flt-" + n).setAttribute("aria-pressed", String(n === name));
|
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();
|
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 ----------
|
// ---------- Züge ----------
|
||||||
|
|
||||||
@@ -156,8 +172,9 @@ async function getJSON(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateHint() {
|
function updateHint() {
|
||||||
const city = document.getElementById("flt-city").getAttribute("aria-pressed") === "true";
|
// nur Trams/U-Bahnen gewählt, aber zu weit weg: sonst wäre die Karte einfach leer
|
||||||
document.getElementById("hint").hidden = !(city && map.getZoom() < URBAN_Z);
|
const onlyCity = !shown.has("long") && !shown.has("local");
|
||||||
|
document.getElementById("hint").hidden = !(onlyCity && map.getZoom() < URBAN_Z);
|
||||||
}
|
}
|
||||||
map.on("zoomend", updateHint);
|
map.on("zoomend", updateHint);
|
||||||
// beim Verschieben/Zoomen Trams nachladen, wenn der Ausschnitt den zuletzt geladenen verlässt
|
// 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");
|
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.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 …"
|
loading.textContent = s.loaded ? "Züge werden geladen …"
|
||||||
: "Fahrplan wird vorbereitet … beim ersten Start dauert das 1–1,5 Stunden.";
|
: "Fahrplan wird vorbereitet … beim ersten Start dauert das 1–1,5 Stunden.";
|
||||||
loading.hidden = false;
|
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
|
async function statusLoop() { // bis alles bereit ist alle 2 s nachfragen, danach alle 30 s
|
||||||
await refreshStatus();
|
await refreshStatus();
|
||||||
setTimeout(statusLoop, ready ? 30000 : 2000);
|
setTimeout(statusLoop, ready && document.getElementById("loading").hidden ? 30000 : 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Zugdetails ----------
|
// ---------- Zugdetails ----------
|
||||||
@@ -371,7 +390,7 @@ async function showTrip(id) {
|
|||||||
document.getElementById("panel").hidden = false;
|
document.getElementById("panel").hidden = false;
|
||||||
map.getSource("route").setData({ type: "Feature", geometry: { type: "LineString", coordinates: t.shape } });
|
map.getSource("route").setData({ type: "Feature", geometry: { type: "LineString", coordinates: t.shape } });
|
||||||
map.getSource("route-stops").setData({ type: "FeatureCollection", features: t.stops.map((x) => (
|
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 ----------
|
// ---------- Suche ----------
|
||||||
@@ -402,8 +421,7 @@ function pick(i) {
|
|||||||
const f = hits[i];
|
const f = hits[i];
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
const now = trains.features.find((x) => x.properties.id === f.properties.id) || f; // aktuelle Position
|
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 (!shown.has(layerOf(f.properties))) setLayers([...shown, layerOf(f.properties)]); // sonst unsichtbar
|
||||||
if (pressed === "local" && f.properties.fv || pressed === "long" && !f.properties.fv) setFilter("all"); // sonst unsichtbar
|
|
||||||
map.flyTo({ center: now.geometry.coordinates, zoom: Math.max(map.getZoom(), 12) });
|
map.flyTo({ center: now.geometry.coordinates, zoom: Math.max(map.getZoom(), 12) });
|
||||||
showTrip(f.properties.id);
|
showTrip(f.properties.id);
|
||||||
document.getElementById("results").hidden = true;
|
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; });
|
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", () => {
|
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);
|
||||||
let filter = "all";
|
let layers = ALL_LAYERS;
|
||||||
try { filter = localStorage.getItem("filter") || "all"; } catch (e) { /* egal */ }
|
try { layers = JSON.parse(localStorage.getItem("layers")) || ALL_LAYERS; } catch (e) { /* egal */ }
|
||||||
setFilter(filter);
|
setLayers(Array.isArray(layers) ? layers : ALL_LAYERS);
|
||||||
statusLoop();
|
statusLoop();
|
||||||
setInterval(refreshTrains, 5000);
|
setInterval(refreshTrains, 5000);
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
|
|||||||
+4
-3
@@ -21,11 +21,12 @@
|
|||||||
<button id="btn-dark" aria-pressed="false">Dunkel</button>
|
<button id="btn-dark" aria-pressed="false">Dunkel</button>
|
||||||
<button id="btn-sat" aria-pressed="false">Satellit</button>
|
<button id="btn-sat" aria-pressed="false">Satellit</button>
|
||||||
</div>
|
</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-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-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>
|
||||||
<div id="search">
|
<div id="search">
|
||||||
<input id="q" type="search" placeholder="Zug suchen, z. B. S5, ICE 11, 2318" autocomplete="off"
|
<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;
|
position: absolute; z-index: 2; background: var(--bg); border-radius: 10px;
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,.18);
|
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 { 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; }
|
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 + .seg { margin-top: 6px; }
|
||||||
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
.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 11px; font: inherit; cursor: pointer; }
|
.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); }
|
.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); }
|
.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