/* ==========================================================================
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 = '