Die Uebersicht auf der Startseite zeigt die Automatiken jetzt als Zeitleiste: eine Bahn je Geraeteart, die Spur ist der Tag. Der Umschalter "Zeitleiste | Liste" oben in der Karte fuehrt zur bisherigen Tabelle zurueck und wird im Browser gemerkt. Jede Automatik erscheint genau einmal, nach festen Regeln (restricted/zeitleiste.php), auch kuenftige: - feste Uhrzeit oder Sonnenstand: Punkt; Zeitfenster mit Messwert: Balken; Kette: hinter ihrem Ausloeser, als gestrichelter Bogen verbunden - ohne jede Zeit: Band "Jederzeit"; pausiert: Band "Pausiert" - an diesem Tag nicht dran (Wochentag, Ferien, Feiertag, Vorabend, wie im Runner gerechnet): gestrichelt mit Grund, nie ausgeblendet - Bahn aus den geschalteten Geraeten ueber bedienform(), Mehrheit gewinnt; Bewaesserung am Geraetetyp - Zaehler "x von y" im Kopf Dazu: Tag vor/zurueck, Etagen einzeln einblendbar (gemerkt), Jetzt-Linie und Nachtschatten aus solarLog.daylight (spaetere Tage vom selben Kalendertag eines Vorjahres), gelaufene Eintraege mit Uhrzeit aus automation_log, Klick oeffnet den Editor. Schmal nur Symbole in den Bahnen und Start bei der aktuellen Uhrzeit. Auf einer Probeseite mit echten Daten geprueft: nichts ragt ueber, keine ueberlappenden Etiketten, alle 22 Automatiken genau einmal, Filter, Tageswechsel und Umschalter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
485 lines
22 KiB
JavaScript
485 lines
22 KiB
JavaScript
/* ==========================================================================
|
||
Automatismen als Zeitleiste eines Tages.
|
||
|
||
Die Daten rechnet der Server (restricted/zeitleiste.php): wo eine
|
||
Automatik auf dem Tag liegt, in welcher Bahn sie steht, ob sie an dem Tag
|
||
dran ist und wann sie gelaufen ist. Hier wird nur gezeichnet.
|
||
|
||
Jede Automatik erscheint genau einmal - in ihrer Bahn, unter "Jederzeit"
|
||
oder unter "Pausiert". Der Zähler oben sagt, wie viele es insgesamt sind;
|
||
fehlte eine, fiele das dort auf.
|
||
|
||
Die bisherige Tabelle bleibt als zweite Ansicht ("Liste"). Welche offen
|
||
ist und welche Etagen eingeblendet sind, merkt sich der Browser.
|
||
========================================================================== */
|
||
|
||
const zeitleiste = {
|
||
datum: null, // "YYYY-MM-DD", null = heute
|
||
daten: null, // Antwort von ?action=zeitleiste
|
||
etagen: null, // Set der eingeblendeten Etagen, null = alle
|
||
uhr: null,
|
||
};
|
||
|
||
const ZL_ZEILE = 30; // Höhe einer Zeile in einer Bahn, px
|
||
const ZL_KOPF = 132; // Breite der Bahnbeschriftung, px
|
||
const ZL_MIN_SPUR = 620; // schmaler wird die Spur nicht, der Rahmen scrollt dann
|
||
|
||
/* --- Kleinigkeiten ------------------------------------------------------- */
|
||
|
||
function zlSpeicher(schluessel, wert) {
|
||
try {
|
||
if (wert === undefined) return localStorage.getItem(schluessel);
|
||
localStorage.setItem(schluessel, wert);
|
||
} catch (e) { return null; }
|
||
return null;
|
||
}
|
||
|
||
function zlText(s) {
|
||
const d = document.createElement("div");
|
||
d.textContent = String(s === undefined || s === null ? "" : s);
|
||
return d.innerHTML.replace(/"/g, """);
|
||
}
|
||
|
||
function zlUhr(m) {
|
||
m = ((Math.round(m) % 1440) + 1440) % 1440;
|
||
return String(Math.floor(m / 60)).padStart(2, "0") + ":" + String(m % 60).padStart(2, "0");
|
||
}
|
||
|
||
function zlIso(d) {
|
||
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
||
}
|
||
|
||
function zlDatumText(iso, heute) {
|
||
const d = new Date(iso + "T12:00:00");
|
||
const tage = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
||
const kurz = tage[d.getDay()] + " " + String(d.getDate()).padStart(2, "0") + "." + String(d.getMonth() + 1).padStart(2, "0") + ".";
|
||
const morgen = new Date(); morgen.setDate(morgen.getDate() + 1);
|
||
if (heute) return "Heute · " + kurz;
|
||
if (iso === zlIso(morgen)) return "Morgen · " + kurz;
|
||
return kurz;
|
||
}
|
||
|
||
/** Textbreite in px, für das Stapeln - einmal ein Canvas, dann nur messen. */
|
||
function zlBreite(text) {
|
||
const c = zlBreite.c || (zlBreite.c = document.createElement("canvas").getContext("2d"));
|
||
c.font = "500 12px " + getComputedStyle(document.body).fontFamily;
|
||
return c.measureText(text).width;
|
||
}
|
||
|
||
/* --- Einordnen ----------------------------------------------------------- */
|
||
|
||
/** Was an Automatiken nach dem Etagenfilter übrig bleibt. */
|
||
function zlSichtbar() {
|
||
const alle = zeitleiste.daten.automatiken;
|
||
return zeitleiste.etagen ? alle.filter(a => zeitleiste.etagen.has(a.floor)) : alle;
|
||
}
|
||
|
||
/** Wo ein Eintrag auf dem Tag beginnt und endet, in Minuten. */
|
||
function zlLage(a) {
|
||
if (a.art === "fenster") return [a.von, a.bis];
|
||
const m = a.laeufe.length ? zlMinuteAus(a.laeufe[a.laeufe.length - 1]) : a.punkt;
|
||
return [m, m];
|
||
}
|
||
|
||
function zlMinuteAus(hhmm) {
|
||
const t = String(hhmm).split(":");
|
||
return Number(t[0]) * 60 + Number(t[1]);
|
||
}
|
||
|
||
/** Zustand eines Eintrags - bestimmt Farbe und Form. */
|
||
function zlZustand(a, d) {
|
||
if (!a.dran) return "nichtdran";
|
||
if (a.laeufe.length) return "gelaufen";
|
||
if (d.jetzt !== null) {
|
||
const ende = a.art === "fenster" ? a.bis : a.punkt;
|
||
if (ende !== null && ende < d.jetzt) return "vorbei";
|
||
const anfang = a.art === "fenster" ? a.von : a.punkt;
|
||
if (anfang !== null && anfang <= d.jetzt) return "aktiv";
|
||
}
|
||
return "kommt";
|
||
}
|
||
|
||
/**
|
||
* Einträge einer Bahn in Zeilen verteilen, wie im Kalender: jeder kommt in
|
||
* die erste Zeile, in der er nichts überdeckt. Gerechnet in px, weil das
|
||
* Etikett mitzählt - zwei Punkte zehn Minuten auseinander liegen zeitlich
|
||
* getrennt, ihre Beschriftungen aber übereinander.
|
||
*/
|
||
function zlStapeln(eintraege, breite) {
|
||
const px = m => m / 1440 * breite;
|
||
const liste = eintraege.map(a => {
|
||
const [von, bis] = zlLage(a);
|
||
const etikett = zlEtikettText(a);
|
||
const lang = zlBreite(etikett) + 26;
|
||
let links = px(von) - 8;
|
||
let rechts = a.art === "fenster" ? Math.max(px(bis), px(von) + lang) : px(von) + lang;
|
||
// Am rechten Rand steht das Etikett links vom Punkt bzw. endet am
|
||
// Balkenende - sonst ragte es aus der Spur und der Rahmen bekaeme einen
|
||
// Scrollbalken fuer ein paar Buchstaben.
|
||
const nachLinks = px(von) + lang > breite - 4;
|
||
if (nachLinks && a.art !== "fenster") { links = px(von) - lang; rechts = px(von) + 8; }
|
||
if (nachLinks && a.art === "fenster") { links = Math.min(px(von), px(bis) - lang) - 8; rechts = px(bis); }
|
||
return { a, links, rechts, nachLinks, etikett };
|
||
}).sort((x, y) => x.links - y.links);
|
||
|
||
const zeilen = [];
|
||
liste.forEach(e => {
|
||
let z = zeilen.findIndex(ende => ende + 6 <= e.links);
|
||
if (z < 0) { z = zeilen.length; zeilen.push(0); }
|
||
zeilen[z] = e.rechts;
|
||
e.zeile = z;
|
||
});
|
||
return { liste, zeilen: Math.max(1, zeilen.length) };
|
||
}
|
||
|
||
/**
|
||
* Die Zeit vorne im Etikett. Beim Punkt die Zeit des Punkts - nach einem
|
||
* Lauf also die echte. Beim Fenster immer das Fenster; der Lauf steht dann
|
||
* hinten mit Haken, sonst stünde "12:50" am Balkenanfang bei 09:30.
|
||
*/
|
||
function zlEtikettZeit(a) {
|
||
if (a.art === "fenster") return zlUhr(a.von) + "–" + zlUhr(a.bis);
|
||
return a.laeufe.length ? a.laeufe[a.laeufe.length - 1] : zlUhr(a.punkt);
|
||
}
|
||
|
||
function zlEtikettText(a) {
|
||
const lauf = a.art === "fenster" && a.laeufe.length ? " ✓ " + a.laeufe[a.laeufe.length - 1] : (a.laeufe.length ? " ✓" : "");
|
||
return zlEtikettZeit(a) + " " + a.name + lauf;
|
||
}
|
||
|
||
/** Der Hinweis beim Überfahren: was, wann, wovon abhängig, wie steht es. */
|
||
function zlTitel(a, zustand) {
|
||
const zeilen = [a.name, a.satz];
|
||
if (a.art === "fenster") zeilen.push("Zeitfenster " + zlUhr(a.von) + "–" + zlUhr(a.bis) + (a.punkt !== null ? ", frühestens " + zlUhr(a.punkt) : ""));
|
||
else if (a.punkt !== null) zeilen.push((a.art === "kette" ? "frühestens " : "um ") + zlUhr(a.punkt) + (a.sonne ? " (Sonnenstand)" : ""));
|
||
if (a.wenn) zeilen.push("Wenn: " + a.wenn);
|
||
const stand = {
|
||
gelaufen: "gelaufen um " + a.laeufe.join(", "),
|
||
nichtdran: "an diesem Tag nicht dran: " + a.grund,
|
||
vorbei: "nicht gelaufen" + (a.wartet ? " – Bedingung war nicht erfüllt" : ""),
|
||
aktiv: a.wartet ? "wartet auf die Bedingung" : "steht an",
|
||
kommt: "kommt noch",
|
||
}[zustand];
|
||
zeilen.push("Stand: " + stand);
|
||
return zeilen.join("\n");
|
||
}
|
||
|
||
/* --- Zeichnen ------------------------------------------------------------ */
|
||
|
||
function zeitleisteZeichnen() {
|
||
const ziel = document.getElementById("zeitleiste");
|
||
const d = zeitleiste.daten;
|
||
if (!ziel || !d || ziel.hidden) return;
|
||
|
||
const sichtbar = zlSichtbar();
|
||
const aktiv = sichtbar.filter(a => a.enabled);
|
||
const pausiert = sichtbar.filter(a => !a.enabled);
|
||
const jederzeit = aktiv.filter(a => a.art === "jederzeit");
|
||
const gelegt = aktiv.filter(a => a.art !== "jederzeit" && (a.punkt !== null || a.von !== null));
|
||
|
||
// --- Kopf: Tag, Etagen, Zähler
|
||
const alleEtagen = !zeitleiste.etagen;
|
||
let html = '<div class="zl-kopf">'
|
||
+ '<div class="btn-group btn-group-sm zl-tag" role="group">'
|
||
+ '<button type="button" class="btn btn-outline-secondary" data-tag="-1" title="Tag davor"><i class="bi bi-chevron-left"></i></button>'
|
||
+ '<button type="button" class="btn btn-outline-secondary zl-tag-name" data-tag="0" title="Zurück zu heute">'
|
||
+ zlText(zlDatumText(d.datum, d.heute)) + "</button>"
|
||
+ '<button type="button" class="btn btn-outline-secondary" data-tag="1" title="Tag danach"><i class="bi bi-chevron-right"></i></button>'
|
||
+ "</div>"
|
||
+ '<div class="zl-etagen" role="group" aria-label="Etagen">'
|
||
+ '<button type="button" class="zl-chip' + (alleEtagen ? " an" : "") + '" data-etage="">Alle</button>'
|
||
+ d.etagen.map(e => {
|
||
const an = alleEtagen || zeitleiste.etagen.has(e.code);
|
||
const anzahl = d.automatiken.filter(a => a.floor === e.code).length;
|
||
return '<button type="button" class="zl-chip' + (an && !alleEtagen ? " an" : "") + (anzahl ? "" : " leer") + '"'
|
||
+ ' data-etage="' + zlText(e.code) + '" title="' + zlText(e.label) + '">'
|
||
+ zlText(e.code) + '<span class="zl-chip-zahl">' + anzahl + "</span></button>";
|
||
}).join("")
|
||
+ "</div>"
|
||
+ '<div class="zl-zahl" title="Jede Automatik steht genau einmal: in ihrer Bahn, unter Jederzeit oder unter Pausiert.">'
|
||
+ '<i class="bi bi-eye"></i> ' + sichtbar.length + " von " + d.automatiken.length
|
||
+ (pausiert.length ? ' <span class="zl-zahl-leise">· ' + pausiert.length + " pausiert</span>" : "")
|
||
+ "</div></div>";
|
||
|
||
// --- Raster. Schmal (Handy) traegt die Bahn nur ihr Symbol - der Name
|
||
// nähme sonst ein Drittel der Breite, und die Spur scrollt ohnehin.
|
||
const rahmenBreite = ziel.clientWidth || 800;
|
||
const schmal = rahmenBreite < 560;
|
||
const kopf = schmal ? 40 : ZL_KOPF;
|
||
const breite = Math.max(ZL_MIN_SPUR, rahmenBreite - kopf - 2);
|
||
const altRahmen = ziel.querySelector(".zl-rahmen");
|
||
const altScroll = altRahmen ? altRahmen.scrollLeft : null;
|
||
const prozent = m => (m / 1440 * 100).toFixed(3) + "%";
|
||
const nacht = d.sonne.auf !== null
|
||
? '<div class="zl-nacht" style="left:0;width:' + prozent(d.sonne.auf) + '"></div>'
|
||
+ '<div class="zl-nacht" style="left:' + prozent(d.sonne.unter) + ';right:0"></div>'
|
||
: "";
|
||
const jetzt = d.jetzt !== null ? '<div class="zl-jetzt" style="left:' + prozent(d.jetzt) + '"></div>' : "";
|
||
|
||
html += '<div class="zl-rahmen' + (schmal ? " schmal" : "") + '"><div class="zl-raster" style="--zl-kopf:' + kopf + "px;min-width:" + (breite + kopf) + 'px">';
|
||
|
||
// Stundenachse mit Sonnenmarken
|
||
html += '<div class="zl-zeile zl-achse"><div class="zl-bahn-kopf"></div><div class="zl-spur">';
|
||
for (let h = 0; h <= 24; h += 3) {
|
||
const rand = h === 0 ? " anfang" : h === 24 ? " ende" : "";
|
||
html += '<span class="zl-stunde' + rand + '" style="left:' + prozent(h * 60) + '">' + h + "</span>";
|
||
}
|
||
if (d.sonne.auf !== null) {
|
||
const geschaetzt = d.sonne.genau ? "" : " (Vorjahr)";
|
||
html += '<span class="zl-sonne" style="left:' + prozent(d.sonne.auf) + '" title="Sonnenaufgang ' + zlUhr(d.sonne.auf) + geschaetzt + '"><i class="bi bi-sunrise"></i></span>'
|
||
+ '<span class="zl-sonne" style="left:' + prozent(d.sonne.unter) + '" title="Sonnenuntergang ' + zlUhr(d.sonne.unter) + geschaetzt + '"><i class="bi bi-sunset"></i></span>';
|
||
}
|
||
if (d.jetzt !== null) html += '<span class="zl-jetzt-marke" style="left:' + prozent(d.jetzt) + '">' + zlUhr(d.jetzt) + "</span>";
|
||
html += "</div></div>";
|
||
|
||
// Bahnen
|
||
const bahnen = Object.keys(d.bahnen).filter(b => gelegt.some(a => a.bahn === b));
|
||
if (!bahnen.length) {
|
||
html += '<div class="zl-leer">Keine Automatik mit einer Zeit auf diesem Tag'
|
||
+ (zeitleiste.etagen ? " in den gewählten Etagen" : "") + ".</div>";
|
||
}
|
||
bahnen.forEach(b => {
|
||
const eintraege = gelegt.filter(a => a.bahn === b);
|
||
const stapel = zlStapeln(eintraege, breite);
|
||
const hoehe = stapel.zeilen * ZL_ZEILE + 8;
|
||
html += '<div class="zl-zeile zl-bahn zl-bahn-' + b + '">'
|
||
+ '<div class="zl-bahn-kopf"><i class="bi ' + d.bahnen[b].symbol + '"></i><span>' + zlText(d.bahnen[b].titel) + "</span>"
|
||
+ '<span class="zl-bahn-zahl">' + eintraege.length + "</span></div>"
|
||
+ '<div class="zl-spur" style="height:' + hoehe + 'px">' + nacht + jetzt;
|
||
stapel.liste.forEach(e => { html += zlEintrag(e, d, prozent); });
|
||
html += "</div></div>";
|
||
});
|
||
|
||
html += '<svg class="zl-ketten" aria-hidden="true"></svg></div></div>';
|
||
|
||
// --- Bänder ohne Lage auf dem Tag
|
||
if (jederzeit.length || pausiert.length) {
|
||
html += '<div class="zl-baender">';
|
||
if (jederzeit.length) {
|
||
html += '<div class="zl-band"><span class="zl-band-titel"><i class="bi bi-infinity"></i>Jederzeit</span>'
|
||
+ '<div class="zl-band-chips">'
|
||
+ jederzeit.map(a => zlChip(a, d.bahnen[a.bahn] ? d.bahnen[a.bahn].symbol : "bi-dot")).join("") + "</div></div>";
|
||
}
|
||
if (pausiert.length) {
|
||
html += '<div class="zl-band zl-band-pause"><span class="zl-band-titel"><i class="bi bi-pause-circle"></i>Pausiert</span>'
|
||
+ '<div class="zl-band-chips">' + pausiert.map(a => zlChip(a, "bi-pause-fill")).join("") + "</div></div>";
|
||
}
|
||
html += "</div>";
|
||
}
|
||
|
||
html += '<div class="zl-legende">'
|
||
+ '<span><i class="zl-l-punkt gelaufen"></i>gelaufen</span>'
|
||
+ '<span><i class="zl-l-punkt"></i>kommt noch</span>'
|
||
+ '<span><i class="zl-l-balken"></i>Zeitfenster, wartet auf Bedingung</span>'
|
||
+ '<span><i class="zl-l-kette"></i>Kette</span>'
|
||
+ '<span><i class="zl-l-punkt nichtdran"></i>an diesem Tag nicht dran</span>'
|
||
+ "</div>";
|
||
|
||
ziel.innerHTML = html;
|
||
zlKettenZeichnen(ziel);
|
||
|
||
// Muss gescrollt werden, steht die Leiste beim ersten Zeichnen bei "jetzt"
|
||
// (an einem anderen Tag beim Morgen) statt bei Mitternacht. Beim
|
||
// Neuzeichnen bleibt sie, wo man sie hingeschoben hat.
|
||
const rahmen = ziel.querySelector(".zl-rahmen");
|
||
if (rahmen && rahmen.scrollWidth > rahmen.clientWidth) {
|
||
if (altScroll !== null && zeitleiste.gescrollt) {
|
||
rahmen.scrollLeft = altScroll;
|
||
} else {
|
||
const ziel_m = d.jetzt !== null ? d.jetzt - 120 : 5 * 60;
|
||
rahmen.scrollLeft = Math.max(0, ziel_m / 1440 * breite);
|
||
zeitleiste.gescrollt = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Ein Eintrag in einer Bahn. */
|
||
function zlEintrag(e, d, prozent) {
|
||
const a = e.a;
|
||
const zustand = zlZustand(a, d);
|
||
const oben = 4 + e.zeile * ZL_ZEILE;
|
||
const titel = zlText(zlTitel(a, zustand));
|
||
const lauf = zustand !== "gelaufen" ? ""
|
||
: ' <i class="bi bi-check2"></i>' + (a.art === "fenster" ? '<span class="zl-lauf">' + zlText(a.laeufe[a.laeufe.length - 1]) + "</span>" : "");
|
||
const etikett = '<span class="zl-etikett"><b>' + zlText(zlEtikettZeit(a)) + "</b>" + zlText(a.name) + lauf + "</span>";
|
||
|
||
if (a.art === "fenster") {
|
||
// Läufe und die früheste Marke als Punkte auf dem Balken.
|
||
let marken = "";
|
||
if (a.punkt !== null && a.punkt > a.von && !a.laeufe.length) {
|
||
marken += '<span class="zl-marke zl-marke-innen" style="left:' + ((a.punkt - a.von) / Math.max(1, a.bis - a.von) * 100).toFixed(2) + '%"></span>';
|
||
}
|
||
a.laeufe.forEach(t => {
|
||
const m = zlMinuteAus(t);
|
||
marken += '<span class="zl-marke zl-marke-innen gelaufen" style="left:' + ((m - a.von) / Math.max(1, a.bis - a.von) * 100).toFixed(2) + '%"></span>';
|
||
});
|
||
return '<div class="zl-ding zl-fenster ' + zustand + (e.nachLinks ? " links" : "") + '" data-id="' + a.id + '" title="' + titel + '"'
|
||
+ ' style="top:' + oben + "px;left:" + prozent(a.von) + ";width:" + prozent(Math.max(10, a.bis - a.von)) + '">'
|
||
+ '<span class="zl-balken">' + marken + "</span>" + etikett + "</div>";
|
||
}
|
||
const m = a.laeufe.length ? zlMinuteAus(a.laeufe[a.laeufe.length - 1]) : a.punkt;
|
||
return '<div class="zl-ding zl-punkt ' + zustand + (e.nachLinks ? " links" : "") + '" data-id="' + a.id + '"'
|
||
+ (a.kette ? ' data-kette="' + a.kette + '"' : "") + ' title="' + titel + '"'
|
||
+ ' style="top:' + oben + "px;left:" + prozent(m) + '">'
|
||
+ '<span class="zl-marke' + (a.sonne ? " sonne" : "") + '"></span>' + etikett + "</div>";
|
||
}
|
||
|
||
function zlChip(a, symbol) {
|
||
return '<button type="button" class="zl-band-chip' + (a.dran ? "" : " nichtdran") + '" data-id="' + a.id + '"'
|
||
+ ' title="' + zlText(a.satz + (a.wenn ? "\nWenn: " + a.wenn : "")) + '">'
|
||
+ '<i class="bi ' + symbol + '"></i>' + zlText(a.name)
|
||
+ '<span class="zl-band-etage">' + zlText(a.floor) + "</span></button>";
|
||
}
|
||
|
||
/**
|
||
* Ketten als gestrichelte Bögen vom Auslöser zum Nachfolger. Erst nach dem
|
||
* Einsetzen, weil die Lage beider Punkte aus dem Layout kommt.
|
||
*/
|
||
function zlKettenZeichnen(ziel) {
|
||
const raster = ziel.querySelector(".zl-raster");
|
||
const svg = ziel.querySelector(".zl-ketten");
|
||
if (!raster || !svg) return;
|
||
const basis = raster.getBoundingClientRect();
|
||
svg.setAttribute("width", basis.width);
|
||
svg.setAttribute("height", basis.height);
|
||
let pfade = "";
|
||
raster.querySelectorAll(".zl-punkt[data-kette]").forEach(kind => {
|
||
const vater = raster.querySelector('.zl-ding[data-id="' + kind.dataset.kette + '"]');
|
||
if (!vater) return;
|
||
const mv = (vater.querySelector(".zl-marke") || vater).getBoundingClientRect();
|
||
const mk = kind.querySelector(".zl-marke").getBoundingClientRect();
|
||
const x1 = mv.left + mv.width / 2 - basis.left, y1 = mv.top + mv.height / 2 - basis.top;
|
||
const x2 = mk.left + mk.width / 2 - basis.left, y2 = mk.top + mk.height / 2 - basis.top;
|
||
const bogen = Math.max(18, Math.abs(y2 - y1) * 0.5);
|
||
pfade += '<path d="M' + x1 + " " + y1 + " C" + (x1 + bogen) + " " + y1 + " " + (x2 - bogen) + " " + y2 + " " + x2 + " " + y2 + '"/>';
|
||
});
|
||
svg.innerHTML = pfade;
|
||
}
|
||
|
||
/* --- Laden und Bedienen -------------------------------------------------- */
|
||
|
||
async function zeitleisteLaden() {
|
||
const ziel = document.getElementById("zeitleiste");
|
||
if (!ziel) return;
|
||
const datum = zeitleiste.datum || zlIso(new Date());
|
||
try {
|
||
const antwort = await fetch("./ajax/AutoAction.php?action=zeitleiste&datum=" + datum,
|
||
{ headers: { "Requested-With-Ajax": "ajax" }, cache: "no-store" });
|
||
const d = await antwort.json();
|
||
if (!antwort.ok || d.error) throw new Error(d.error || "HTTP " + antwort.status);
|
||
zeitleiste.daten = d;
|
||
// Etagen, die es nicht mehr gibt, fallen aus dem Filter.
|
||
if (zeitleiste.etagen) {
|
||
const codes = new Set(d.etagen.map(e => e.code));
|
||
zeitleiste.etagen = new Set([...zeitleiste.etagen].filter(c => codes.has(c)));
|
||
if (!zeitleiste.etagen.size) zeitleiste.etagen = null;
|
||
}
|
||
zeitleisteZeichnen();
|
||
} catch (e) {
|
||
ziel.innerHTML = '<div class="alert alert-danger mb-0">Zeitleiste nicht abrufbar: ' + zlText(e.message) + "</div>";
|
||
}
|
||
}
|
||
|
||
function zlAnsicht(welche) {
|
||
const zl = document.getElementById("zeitleiste");
|
||
const liste = document.getElementById("automatikListe");
|
||
if (!zl || !liste) return;
|
||
zl.hidden = welche !== "zeitleiste";
|
||
liste.hidden = welche === "zeitleiste";
|
||
document.querySelectorAll(".zl-ansicht [data-ansicht]").forEach(k =>
|
||
k.classList.toggle("active", k.dataset.ansicht === welche));
|
||
zlSpeicher("automatik.ansicht", welche);
|
||
if (welche === "zeitleiste") {
|
||
if (zeitleiste.daten) zeitleisteZeichnen(); else zeitleisteLaden();
|
||
}
|
||
}
|
||
|
||
function zlEtagenMerken() {
|
||
zlSpeicher("automatik.etagen", zeitleiste.etagen ? [...zeitleiste.etagen].join(",") : "");
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
const ziel = document.getElementById("zeitleiste");
|
||
if (!ziel) return;
|
||
|
||
const gemerkt = zlSpeicher("automatik.etagen");
|
||
zeitleiste.etagen = gemerkt ? new Set(gemerkt.split(",").filter(Boolean)) : null;
|
||
if (zeitleiste.etagen && !zeitleiste.etagen.size) zeitleiste.etagen = null;
|
||
|
||
document.querySelectorAll(".zl-ansicht [data-ansicht]").forEach(k =>
|
||
k.addEventListener("click", () => zlAnsicht(k.dataset.ansicht)));
|
||
zlAnsicht(zlSpeicher("automatik.ansicht") === "liste" ? "liste" : "zeitleiste");
|
||
|
||
// Neue Automatik: in der ersten eingeblendeten Etage, sonst der Standard.
|
||
const neu = document.querySelector(".zl-neu");
|
||
if (neu) neu.addEventListener("click", function () {
|
||
let etage = typeof homeStandardEtage !== "undefined" ? homeStandardEtage : "";
|
||
if (zeitleiste.etagen && zeitleiste.daten) {
|
||
const erste = zeitleiste.daten.etagen.find(e => zeitleiste.etagen.has(e.code));
|
||
if (erste) etage = erste.code;
|
||
}
|
||
const liste = document.getElementById("automatikListe");
|
||
if (liste && !liste.hidden) {
|
||
const offen = document.querySelector("#actions-tabContent .tab-pane.active");
|
||
if (offen) etage = offen.id.replace(/^actions-/, "");
|
||
}
|
||
openAutoActionModal("?action=editor&floor=" + encodeURIComponent(etage));
|
||
});
|
||
|
||
ziel.addEventListener("click", function (ereignis) {
|
||
const t = ereignis.target;
|
||
const ding = t.closest("[data-id]");
|
||
if (ding) {
|
||
openAutoActionModal("?action=editor&id=" + ding.dataset.id);
|
||
return;
|
||
}
|
||
const tag = t.closest("[data-tag]");
|
||
if (tag) {
|
||
const schritt = Number(tag.dataset.tag);
|
||
if (schritt === 0) {
|
||
zeitleiste.datum = null;
|
||
} else {
|
||
const d = new Date((zeitleiste.datum || zlIso(new Date())) + "T12:00:00");
|
||
d.setDate(d.getDate() + schritt);
|
||
zeitleiste.datum = zlIso(d) === zlIso(new Date()) ? null : zlIso(d);
|
||
}
|
||
zeitleisteLaden();
|
||
return;
|
||
}
|
||
const chip = t.closest("[data-etage]");
|
||
if (chip && zeitleiste.daten) {
|
||
const code = chip.dataset.etage;
|
||
if (!code) {
|
||
zeitleiste.etagen = null;
|
||
} else if (!zeitleiste.etagen) {
|
||
// Aus "alle" heraus blendet ein Klick genau diese Etage ein.
|
||
zeitleiste.etagen = new Set([code]);
|
||
} else {
|
||
zeitleiste.etagen.has(code) ? zeitleiste.etagen.delete(code) : zeitleiste.etagen.add(code);
|
||
if (!zeitleiste.etagen.size || zeitleiste.etagen.size === zeitleiste.daten.etagen.length) {
|
||
zeitleiste.etagen = null;
|
||
}
|
||
}
|
||
zlEtagenMerken();
|
||
zeitleisteZeichnen();
|
||
}
|
||
});
|
||
|
||
// Neu zeichnen, wenn sich die Breite ändert - Stapeln und Ketten hängen daran.
|
||
let warte = null;
|
||
if (typeof ResizeObserver !== "undefined") {
|
||
let letzteBreite = 0;
|
||
new ResizeObserver(() => {
|
||
if (Math.abs(ziel.clientWidth - letzteBreite) < 4) return;
|
||
letzteBreite = ziel.clientWidth;
|
||
clearTimeout(warte);
|
||
warte = setTimeout(zeitleisteZeichnen, 120);
|
||
}).observe(ziel);
|
||
}
|
||
|
||
// Heute rückt die Jetzt-Linie weiter, und neue Läufe kommen dazu.
|
||
zeitleiste.uhr = setInterval(() => {
|
||
if (!ziel.hidden && !zeitleiste.datum && !document.hidden) zeitleisteLaden();
|
||
}, 5 * 60 * 1000);
|
||
});
|