Files
Smart-Dashboard/js/solar/energiefluss.js
T

607 lines
26 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
Energiefluss-Übersicht: zeichnet den Katalog (energieflussKatalog.js) als
animiertes SVG und hält es mit den MQTT-Werten aktuell.
const bild = new Energiefluss(behaelter, {
einstellungen, // Abweichungen aus homeMesh.energiefluss
bearbeiten: false, // true = Einstellungsvorschau, Kreise ziehbar
onKlick(knoten), // Kreis mit klick-Ziel angetippt
onVerschoben(id, x, y), // nur beim Bearbeiten
onGezeichnet(), // nach jedem (Neu-)Zeichnen
hochkantUnter: 720, // optional: px Breite, darunter hochkant (0 = nie)
});
bild.aktualisieren(mqttData);
Der Renderer kennt keinen Kreis beim Namen. Alles, was einen Kreis
ausmacht, steht im Katalog; hier steht nur, wie Kreise, Flüsse, Symbole
und Plaketten grundsätzlich aussehen.
Zwei Ansichten: quer (Lage aus dem Katalog) und hochkant für schmale
Schirme. Hochkant sind x und y vertauscht - aus "Quellen links, Verbraucher
rechts" wird "Quellen oben, Verbraucher unten". So braucht es keine zweite
Lage je Kreis, und eine eigene Lage aus den Einstellungen gilt in beiden.
Zur Leistung: die Flüsse laufen über stroke-dashoffset in groben Stufen
(steps), und alle Animationen stehen still, solange die Karte nicht zu
sehen ist. Werte werden nur geschrieben, wenn sie sich geändert haben.
========================================================================== */
(function () {
"use strict";
const NS = "http://www.w3.org/2000/svg";
const RADIUS = 78;
const RADIUS_GROSS = 92;
const RAND = 60; // Luft um die äußersten Kreise
const HOCHKANT_UNTER = 720; // px Breite, ab der hochkant gezeichnet wird
/* ------------------------------------------------------------------------
Symbole: 24er Raster, Mitte 0/0, Linien in currentColor. Teile mit
Klasse enf-s-* werden über CSS animiert oder über den Zustand gesetzt.
------------------------------------------------------------------------ */
const SYMBOLE = {
sonne: () => `
<g class="enf-s-strahlen">
${[0, 45, 90, 135, 180, 225, 270, 315].map(w =>
`<path d="M0-8.2V-11" transform="rotate(${w})"/>`).join("")}
</g>
<circle r="4.6" class="enf-s-kern"/>`,
netz: () => `
<path d="M-5 11.5L-1.6-10.5h3.2L5 11.5"/>
<path d="M-9.5-5.5h19M-7-1h14"/>
<path d="M-8.5-5.5v2.4M8.5-5.5v2.4M-6-1v2.4M6-1v2.4"/>
<path d="M-3.3 3.5l6.8 8M3.3 3.5l-6.8 8" class="enf-s-fein"/>`,
akku: () => `
<rect x="-6.5" y="-8.5" width="13" height="20" rx="2.4"/>
<path d="M-2.6-11h5.2"/>
<rect x="-4" y="-6" width="8" height="15" rx="1" class="enf-s-stand"/>
<path d="M1.2-4.2l-4 5.6h3l-1.4 4.6 4.2-6h-3z" class="enf-s-blitz"/>`,
wechselrichter: () => `
<rect x="-10.5" y="-10.5" width="21" height="21" rx="3.5"/>
<path d="M-10.5 10.5l21-21" class="enf-s-fein"/>
<path d="M-7.5-5.2h5M-7.5-2.4h5"/>
<path d="M1.8 4.2c1-1.7 2.1-1.7 3.1 0s2.1 1.7 3.1 0" class="enf-s-welle"/>`,
verbrauch: () => `
<path d="M-11-1.5L0-10.5l11 9"/>
<path d="M-8-4v14.5h16V-4"/>
<path d="M1.3-3.8l-4.4 6.2h3.3l-1.6 5 4.6-6.6H-.1z" class="enf-s-blitz enf-s-an"/>`,
etageOben: () => etage(-4.5, 1.5),
etageMitte: () => etage(1.5, 7.5),
etageUnten: () => etage(7.5, 11.5),
wallbox: () => `
<rect x="-10.5" y="-10.5" width="11" height="21" rx="2.2"/>
<rect x="-8" y="-7.5" width="6" height="4" rx=".8" class="enf-s-fein"/>
<path d="M-6.4 1.2l-2 3.5h2.2l-1 3.2 2.9-4.2h-2.2z" class="enf-s-blitz"/>
<path d="M.5 2h2.8a2.3 2.3 0 0 1 2.3 2.3v3.9a1.9 1.9 0 0 0 3.8 0V-.5" class="enf-s-kabel"/>
<path d="M7.2-4.6h3.6v2.3a1.8 1.8 0 0 1-3.6 0z"/>
<path d="M8.2-4.6v-2M9.8-4.6v-2"/>`,
speicher: () => `
<defs><clipPath id="{id}-tank"><rect x="-6.8" y="-10.8" width="13.6" height="19.6" rx="5.2"/></clipPath></defs>
<g clip-path="url(#{id}-tank)" class="enf-s-temps">
<rect x="-8" y="-12" width="16" height="7.6" class="enf-s-t0"/>
<rect x="-8" y="-4.4" width="16" height="6.6" class="enf-s-t1"/>
<rect x="-8" y="2.2" width="16" height="7.6" class="enf-s-t2"/>
</g>
<rect x="-7" y="-11" width="14" height="20" rx="5.4"/>
<path d="M-3.4 5.6l1.1-1.8 1.2 1.8 1.1-1.8 1.2 1.8 1.1-1.8" class="enf-s-spirale"/>
<path d="M-4 9v2.5M4 9v2.5"/>`,
tropfen: () => `
<path d="M0-11c4.6 5.5 7 9 7 12.6a7 7 0 0 1-14 0C-7-2-4.6-5.5 0-11z"/>
<path d="M-3.6 3.4c1.2 1 2.4 1 3.6 0s2.4-1 3.6 0" class="enf-s-welle"/>`,
};
/* Kleine Symbole für Zeilen in einem Kreis (Bewässerungszonen): 24er
Raster, Ecke oben links, Linien in currentColor. */
const ZONENBILDER = {
// Pflanzkübel mit Pflanze - ein Trog auf der Terrasse
kuebel: `
<path d="M12 12V7.2"/>
<path d="M12 9.6c-2.6.1-4.3-1.5-4.4-4.2 2.6-.1 4.3 1.5 4.4 4.2z"/>
<path d="M12 8.4c.1-2.7 1.8-4.3 4.4-4.2-.1 2.7-1.8 4.3-4.4 4.2z"/>
<rect x="4.6" y="12" width="14.8" height="2.8" rx=".8"/>
<path d="M6.2 14.8 7.6 21h8.8l1.4-6.2"/>`,
// Baum im Gras - der normale Garten
garten: `
<path d="M12 3.6a4.3 4.3 0 0 0-4.2 5.3 3.4 3.4 0 0 0 1.3 6.5h5.8a3.4 3.4 0 0 0 1.3-6.5A4.3 4.3 0 0 0 12 3.6z"/>
<path d="M12 15.4V20.6M12 18l2-1.8"/>
<path d="M3.5 20.6h17M6 20.6l-.7-1.9M7.6 20.6l.3-2.1M17.2 20.6l.6-2M18.6 20.6l.9-1.6"/>`,
// Hochbeet: Holzkasten auf Füßen, oben drei Pflanzen
hochbeet: `
<path d="M8 11V8.6"/>
<path d="M8 9.4c-1.5 0-2.4-.9-2.4-2.4 1.5 0 2.4.9 2.4 2.4z"/>
<path d="M12 11V6.4"/>
<path d="M12 8.6c-1.6 0-2.6-1-2.6-2.6 1.6 0 2.6 1 2.6 2.6z"/>
<path d="M12 7.6c0-1.6 1-2.6 2.6-2.6 0 1.6-1 2.6-2.6 2.6z"/>
<path d="M16 11V8.6"/>
<path d="M16 9.4c0-1.5.9-2.4 2.4-2.4 0 1.5-.9 2.4-2.4 2.4z"/>
<rect x="3.5" y="11" width="17" height="8.4" rx=".8"/>
<path d="M3.5 13.8h17M3.5 16.6h17" stroke-width="1.1"/>
<path d="M5.4 19.4v1.9M18.6 19.4v1.9"/>`,
};
function etage(von, bis) {
return `
<rect x="-8" y="${von}" width="16" height="${bis - von}" class="enf-s-ebene"/>
<path d="M-11.5-3L0-11l11.5 8"/>
<path d="M-8-5v12.5h16V-5"/>
<path d="M-8 1.5h16" class="enf-s-fein"/>
<path d="M-12 7.5h24"/>
<path d="M-8 7.5v4h16v-4" class="enf-s-fein enf-s-keller"/>`;
}
/* Plaketten am Kreis: Zeichen aus Bootstrap Icons (liegt ohnehin im Dashboard). */
const ZEICHEN = {
// Codepunkte als Escape: die Zeichen selbst liegen im privaten Bereich
// (U+F90C sogar auf einem CJK-Kompatibilitaetszeichen) und sind im Editor
// unsichtbar oder werden beim Kopieren leicht verfaelscht.
eco: { glyph: "\uF90C", farbe: "#6cbe58" }, // bi-leaf
uhr: { glyph: "\uF291", farbe: "#e8a33a" }, // bi-clock-fill
manuell: { glyph: "\uF56B", farbe: "#c9a227" }, // bi-sliders
stecker: { glyph: "\uF4F6", farbe: "#8fb8de" }, // bi-plug-fill
schloss: { glyph: "\uF47A", farbe: "#b9c0c8" }, // bi-lock-fill
heizung: { glyph: "\uF7F6", farbe: "#e5624f" }, // bi-fire
tank: { glyph: "\uF83D", farbe: "#b9c0c8" }, // bi-fuel-pump-fill
warnung: { glyph: "\uF33A", farbe: "#e5624f" }, // bi-exclamation-triangle-fill
};
/* ------------------------------------------------------------------------
Hilfen
------------------------------------------------------------------------ */
function el(name, attr, eltern) {
const e = document.createElementNS(NS, name);
if (attr) for (const k in attr) e.setAttribute(k, attr[k]);
if (eltern) eltern.appendChild(e);
return e;
}
function leistungText(w) {
const a = Math.abs(w);
if (a < 1000) return Math.round(a) + "W";
return (a / 1000).toFixed(a < 10000 ? 2 : 1).replace(".", ",") + "kW";
}
/** Liest mqttData: Werte kommen als Buffer, Zahl und Text wie gehabt. */
function leser(daten) {
const cache = {};
return {
text(topic) {
const v = daten[topic];
return v === undefined ? undefined : String(v);
},
zahl(topic) {
const v = Number(String(daten[topic]));
return isFinite(v) ? v : 0;
},
json(topic) {
if (topic in cache) return cache[topic];
try { cache[topic] = JSON.parse(String(daten[topic])); } catch (e) { cache[topic] = null; }
return cache[topic];
},
};
}
/** Farbe zwischen kalt und heiß, wie früher assignColor() fürs Pufferbild. */
function tempFarbe(t) {
const f = Math.max(0, Math.min(1, (t - 20) / 60));
const kalt = [35, 137, 186], heiss = [186, 59, 35];
return "rgb(" + kalt.map((k, i) => Math.round(k + (heiss[i] - k) * f)).join(",") + ")";
}
let laufendeNummer = 0;
/* ------------------------------------------------------------------------
Die Übersicht
------------------------------------------------------------------------ */
class Energiefluss {
constructor(behaelter, opt = {}) {
this.behaelter = behaelter;
this.opt = opt;
this.bearbeiten = !!opt.bearbeiten;
this.nummer = ++laufendeNummer;
this.letzte = null;
this.setzeEinstellungen(opt.einstellungen || {});
// Hochkant oder quer - neu zeichnen nur, wenn die Ansicht wechselt.
this.beobachter = new ResizeObserver(() => {
const hoch = this.hochkantGewuenscht();
if (hoch !== this.hochkant) this.zeichnen();
});
this.beobachter.observe(behaelter);
// Animationen anhalten, solange niemand hinsieht.
this.sichtbar = true;
this.sicht = new IntersectionObserver(eintraege => {
this.sichtbar = eintraege[0].isIntersecting;
this.pausieren();
});
this.sicht.observe(behaelter);
document.addEventListener("visibilitychange", () => this.pausieren());
}
/** Katalog und Abweichungen zu einer Liste zusammenführen. */
setzeEinstellungen(einstellungen) {
this.einstellungen = einstellungen;
this.knoten = ENERGIEFLUSS_KATALOG.knoten.map(k => {
const e = einstellungen[k.id] || {};
return Object.assign({}, k, {
aktiv: k.pflicht || e.aktiv !== false,
anzeigeName: e.name || k.name,
lage: [e.x ?? k.x, e.y ?? k.y],
optionenWirksam: Object.assign({}, k.optionen || {}, e.optionen || {}),
});
});
this.nachId = Object.fromEntries(this.knoten.map(k => [k.id, k]));
// Wirksamer Elternkreis: der nächste eingeschaltete in der Kette.
for (const k of this.knoten) {
let e = k.eltern ? this.nachId[k.eltern] : null;
while (e && !e.aktiv) e = e.eltern ? this.nachId[e.eltern] : null;
k.elternWirksam = e;
let tiefe = 0;
for (let p = e; p; p = p.elternWirksam) tiefe++;
k.tiefe = tiefe;
}
}
/** Antippbar: hat ein Ziel und wird nicht gerade bearbeitet. */
klickbar(k) {
return !!k.klick && !this.bearbeiten;
}
hochkantGewuenscht() {
const grenze = this.opt.hochkantUnter ?? HOCHKANT_UNTER;
return !this.bearbeiten && this.behaelter.clientWidth > 0 && this.behaelter.clientWidth < grenze;
}
/** Lage eines Kreises in der aktuellen Ansicht. */
punkt(k) {
return this.hochkant ? [k.lage[1], k.lage[0]] : [k.lage[0], k.lage[1]];
}
radius(k) {
return k.gross ? RADIUS_GROSS : RADIUS;
}
sichtbareKnoten() {
return this.knoten.filter(k => k.aktiv || this.bearbeiten);
}
/* ---------------------------------------------------------- Zeichnen */
zeichnen() {
this.hochkant = this.hochkantGewuenscht();
this.behaelter.innerHTML = "";
const svg = this.svg = el("svg", {
class: "enf" + (this.bearbeiten ? " enf-bearbeiten" : "") + (this.hochkant ? " enf-hoch" : ""),
role: "img", "aria-label": "Energiefluss",
}, this.behaelter);
this.ebeneFluss = el("g", { class: "enf-fluesse" }, svg);
this.ebeneKnoten = el("g", { class: "enf-knotenebene" }, svg);
this.elemente = {};
for (const k of this.sichtbareKnoten()) this.knotenZeichnen(k);
for (const k of this.sichtbareKnoten()) if (k.aktiv && k.elternWirksam) this.flussZeichnen(k);
this.rahmenSetzen();
this.pausieren();
if (this.letzte) {
const d = this.letzte;
this.letzte = null;
this.aktualisieren(d);
}
if (this.opt.onGezeichnet) this.opt.onGezeichnet();
}
rahmenSetzen() {
const ks = this.sichtbareKnoten();
if (!ks.length) return;
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const k of ks) {
const [x, y] = this.punkt(k), r = this.radius(k);
x0 = Math.min(x0, x - r - 20); x1 = Math.max(x1, x + r + 20);
y0 = Math.min(y0, y - r - 20); y1 = Math.max(y1, y + r + 56); // Name unter dem Kreis
}
this.svg.setAttribute("viewBox",
[x0 - RAND / 2, y0 - RAND / 2, x1 - x0 + RAND, y1 - y0 + RAND].map(Math.round).join(" "));
}
knotenZeichnen(k) {
const [x, y] = this.punkt(k);
const r = this.radius(k);
const id = "enf" + this.nummer + "-" + k.id;
const g = el("g", {
class: "enf-knoten" + (k.aktiv ? "" : " enf-aus") + (this.klickbar(k) ? " enf-klickbar" : ""),
transform: `translate(${x} ${y})`,
style: `--farbe:${k.farbe};--verz:${k.tiefe * 110}ms`,
"data-knoten": k.id,
}, this.ebeneKnoten);
const koerper = el("g", { class: "enf-koerper" }, g);
el("circle", { r: r + 17, class: "enf-hof" }, koerper);
el("circle", { r, class: "enf-spur" }, koerper);
const ring = el("g", { class: "enf-ring", transform: "rotate(-90)" }, koerper);
el("circle", { r: r - 11, class: "enf-scheibe" }, koerper);
// Füllstand (Batterie, Auto): Welle, die mit dem Stand steigt.
let fuellung = null;
if (k.symbol === "akku" || k.symbol === "wallbox") {
el("clipPath", { id: id + "-innen" }, el("defs", null, koerper))
.appendChild(el("circle", { r: r - 11 }));
const clip = el("g", { "clip-path": `url(#${id}-innen)` }, koerper);
fuellung = el("g", { class: "enf-fuellung", style: `--stand:0;--r:${r - 11}` }, clip);
const b = r * 4;
el("path", {
class: "enf-welle",
d: `M${-b} 0 q${r / 2} -9 ${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 t${r} 0 V${r * 2} H${-b} Z`,
}, fuellung);
}
const symbol = el("g", {
class: "enf-symbol enf-sym-" + k.symbol,
transform: `translate(0 ${-r * 0.43}) scale(${k.gross ? 1.95 : 1.75})`,
}, koerper);
symbol.innerHTML = SYMBOLE[k.symbol]().replace(/\{id\}/g, id);
// Die kleine Zeile sitzt dicht unter dem Wert: weiter unten wird die
// Scheibe schmal, und "89 % entlädt" stößt an den Rand.
const wert = el("text", { class: "enf-wert", y: r * 0.2 + 6 }, koerper);
const neben = el("text", { class: "enf-neben", y: r * 0.2 + 26 }, koerper);
const name = el("text", { class: "enf-name", y: r + 46 }, g);
name.textContent = k.anzeigeName;
const zeichen = el("g", { class: "enf-zeichen" }, koerper);
if (this.klickbar(k)) {
const t = el("g", { class: "enf-tipp", transform: `translate(${r * 0.72} ${-r * 0.72})` }, koerper);
el("circle", { r: 15 }, t);
el("path", { d: "M-3.5-6l6 6-6 6" }, t);
g.setAttribute("tabindex", "0");
g.setAttribute("role", "button");
g.setAttribute("aria-label", k.anzeigeName + " Details");
const los = () => this.opt.onKlick && this.opt.onKlick(k);
g.addEventListener("click", los);
g.addEventListener("keydown", e => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); los(); }
});
}
// Bewässerung: Zonen als Zeilen im Kreis. Die Ids gehören solarMQTT.js
// und werden nur außerhalb der Einstellungsvorschau vergeben.
if (k.zonen) {
wert.remove(); neben.remove();
symbol.setAttribute("transform", `translate(0 ${-r * 0.5}) scale(1.4)`);
k.zonen.forEach((z, i) => {
const zy = -r * 0.1 + i * 25;
// Eigenes Symbol (ZONENBILDER) oder ein Zeichen aus Bootstrap Icons.
// updateWateringIndicator() färbt es über color und schreibt den
// Tooltip in das <title>.
let glyph;
if (z.bild && ZONENBILDER[z.bild]) {
glyph = el("g", { class: "enf-zone-bild", transform: `translate(${(-r * 0.52 - 9.6).toFixed(1)} ${(zy - 9.6).toFixed(1)}) scale(.8)` }, koerper);
glyph.innerHTML = ZONENBILDER[z.bild];
} else {
glyph = el("text", { class: "enf-zone-glyph", x: -r * 0.52, y: zy }, koerper);
glyph.textContent = z.zeichen || "";
}
el("title", null, glyph).textContent = z.name;
const nm = el("text", { class: "enf-zone-name", x: -r * 0.36, y: zy }, koerper);
nm.textContent = z.name;
const st = el("text", { class: "enf-zone-status", x: r * 0.6, y: zy }, koerper);
st.textContent = "Aus";
if (!this.bearbeiten) { glyph.id = z.symbol; st.id = z.status; }
});
}
if (this.bearbeiten) this.ziehbarMachen(g, k);
this.elemente[k.id] = { g, ring, wert, neben, zeichen, symbol, fuellung, fluss: null, segmente: [] };
}
flussPfad(k) {
const e = k.elternWirksam;
const a = this.punkt(e), b = this.punkt(k);
// Knick: quer vom Elternkreis senkrecht los, dann waagerecht in den Kreis
// (Ecke x vom Eltern, y vom Kind); hochkant genau umgekehrt.
let c;
if (Math.abs(a[0] - b[0]) < 30 || Math.abs(a[1] - b[1]) < 30) {
c = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
} else {
c = this.hochkant ? [b[0], a[1]] : [a[0], b[1]];
}
const rand = (p, q, r) => {
const dx = q[0] - p[0], dy = q[1] - p[1], l = Math.hypot(dx, dy) || 1;
return [p[0] + dx / l * (r + 20), p[1] + dy / l * (r + 20)];
};
const s = rand(a, c, this.radius(e)), z = rand(b, c, this.radius(k));
return `M${s[0].toFixed(1)} ${s[1].toFixed(1)} Q${c[0]} ${c[1]} ${z[0].toFixed(1)} ${z[1].toFixed(1)}`;
}
flussZeichnen(k) {
const d = this.flussPfad(k);
const g = el("g", { class: "enf-fluss enf-still", style: `--farbe:${k.farbe};--verz:${k.tiefe * 110 + 150}ms` }, this.ebeneFluss);
const bett = el("path", { d, class: "enf-bett", pathLength: "1" }, g);
const hof = el("path", { d, class: "enf-strom-hof" }, g);
const strom = el("path", { d, class: "enf-strom" }, g);
this.elemente[k.id].fluss = { g, bett, hof, strom };
}
/* ------------------------------------------------------ Aktualisieren */
aktualisieren(daten) {
this.letzte = daten;
if (!this.svg) return;
const m = leser(daten);
for (const k of this.sichtbareKnoten()) {
const e = this.elemente[k.id];
if (!e) continue;
let z;
try {
z = k.lesen(m, k.optionenWirksam) || {};
} catch (fehler) {
console.warn("Energiefluss: " + k.id, fehler);
continue;
}
this.knotenSetzen(k, e, z);
if (e.fluss) this.flussSetzen(e.fluss, z.fluss || 0);
}
}
knotenSetzen(k, e, z) {
// Werte
const wert = z.wert !== undefined ? z.wert : leistungText(z.leistung || 0);
if (e.wert.isConnected && e.wert.textContent !== wert) e.wert.textContent = wert;
const neben = z.neben || "";
if (e.neben.isConnected && e.neben.textContent !== neben) e.neben.textContent = neben;
e.g.classList.toggle("enf-ruhe", !!z.ruhe && k.aktiv);
// Ring: Segmente in Hundertsteln des Umfangs
let segmente = z.segmente || (z.leistung !== undefined ? [{ w: Math.abs(z.leistung), farbe: k.farbe }] : []);
segmente = this.segmenteZusammenfassen(segmente);
const skala = k.skala || 15000;
while (e.segmente.length < segmente.length) {
e.segmente.push(el("circle", { r: this.radius(k), class: "enf-bogen", pathLength: "100" }, e.ring));
}
let start = 0;
e.segmente.forEach((kreis, i) => {
const s = segmente[i];
let laenge = s ? Math.max(0, Math.min(100 - start, (s.w / skala) * 100)) : 0;
if (laenge < 0.4) laenge = 0;
const luecke = laenge > 0 && start > 0 ? 0.6 : 0;
const a = (laenge - luecke).toFixed(2), off = (-(start + luecke)).toFixed(2);
const schluessel = a + "|" + off + "|" + (s ? s.farbe : "");
if (kreis._stand !== schluessel) {
kreis._stand = schluessel;
kreis.style.strokeDasharray = `${Math.max(0, a)} 100`;
kreis.style.strokeDashoffset = off;
if (s) kreis.style.stroke = s.farbe;
}
start += laenge;
});
// Zustand des Symbols
const zu = z.zustand || {};
e.g.classList.toggle("enf-an", !!zu.an);
e.g.classList.toggle("enf-laden", !!zu.laden);
if (zu.stand !== undefined && isFinite(zu.stand)) {
const s = Math.max(0, Math.min(1, zu.stand)).toFixed(3);
if (e.g.style.getPropertyValue("--stand") !== s) {
e.g.style.setProperty("--stand", s);
if (e.fuellung) e.fuellung.style.setProperty("--stand", s);
}
}
if (zu.temps) {
zu.temps.forEach((t, i) => {
const r = e.symbol.querySelector(".enf-s-t" + i);
const f = tempFarbe(t);
if (r && r._f !== f) { r._f = f; r.style.fill = f; }
});
}
// Plaketten
const zeichen = z.zeichen || [];
const sig = zeichen.map(x => x.art + ":" + (x.text || "") + ":" + (x.titel || "")).join("|");
if (e.zeichen._sig !== sig) {
e.zeichen._sig = sig;
this.zeichenSetzen(k, e.zeichen, zeichen);
}
}
/** Anteile ausgeschalteter Kreise fallen ins letzte Segment. */
segmenteZusammenfassen(segmente) {
const aus = segmente.filter(s => s.knoten && this.nachId[s.knoten] && !this.nachId[s.knoten].aktiv);
if (!aus.length) return segmente;
const rest = segmente.filter(s => !aus.includes(s)).map(s => Object.assign({}, s));
if (rest.length) rest[rest.length - 1].w += aus.reduce((a, s) => a + Math.max(0, s.w), 0);
return rest;
}
zeichenSetzen(k, gruppe, zeichen) {
gruppe.innerHTML = "";
const r = this.radius(k);
zeichen.forEach((z, i) => {
const art = ZEICHEN[z.art];
if (!art) return;
// Außen am Ring rechts unten, eine Plakette je 36 Grad - so bleiben
// Wert und Zeile darunter frei.
const winkel = (88 + i * 36) * Math.PI / 180;
const px = Math.sin(winkel) * (r + 12), py = -Math.cos(winkel) * (r + 12);
const p = el("g", { class: "enf-plakette", transform: `translate(${px.toFixed(1)} ${py.toFixed(1)})`, style: `--pfarbe:${art.farbe};--i:${i}` }, gruppe);
if (z.titel) el("title", null, p).textContent = z.titel;
if (z.text) {
const breite = 26 + z.text.length * 10;
el("rect", { x: -16, y: -16, width: breite, height: 32, rx: 16 }, p);
el("text", { class: "enf-plakette-text", x: 12, y: 1 }, p).textContent = z.text;
} else {
el("circle", { r: 16 }, p);
}
el("text", { class: "enf-plakette-glyph", x: 0, y: 1 }, p).textContent = art.glyph;
});
}
flussSetzen(f, w) {
const a = Math.abs(w);
const still = a < 15;
const breite = still ? 0 : 3.5 + 6.5 * (1 - Math.exp(-a / 3000));
// Schneller bei mehr Leistung: 2,6 s je Abschnitt bei wenig, 0,7 s bei viel.
const dauer = still ? 0 : 0.7 + 1.9 * Math.exp(-a / 3000);
const sig = still ? "still" : breite.toFixed(1) + "|" + dauer.toFixed(2) + "|" + (w < 0);
if (f._sig === sig) return;
f._sig = sig;
f.g.classList.toggle("enf-still", still);
f.g.classList.toggle("enf-rueck", w < 0);
f.g.style.setProperty("--breite", breite.toFixed(1));
f.g.style.setProperty("--dauer", dauer.toFixed(2) + "s");
}
pausieren() {
if (!this.svg) return;
this.svg.classList.toggle("enf-pause", !this.sichtbar || document.hidden);
}
/* -------------------------------------------------------- Bearbeiten */
ziehbarMachen(g, k) {
g.classList.add("enf-ziehbar");
g.addEventListener("pointerdown", ev => {
ev.preventDefault();
g.setPointerCapture(ev.pointerId);
const pt = this.svg.createSVGPoint();
const svgPunkt = e2 => {
pt.x = e2.clientX; pt.y = e2.clientY;
return pt.matrixTransform(this.svg.getScreenCTM().inverse());
};
const start = svgPunkt(ev);
const [x0, y0] = k.lage;
g.classList.add("enf-zieht");
const bewegen = e2 => {
const p = svgPunkt(e2);
k.lage = [Math.round((x0 + p.x - start.x) / 10) * 10, Math.round((y0 + p.y - start.y) / 10) * 10];
g.setAttribute("transform", `translate(${k.lage[0]} ${k.lage[1]})`);
for (const kind of this.knoten) {
const f = this.elemente[kind.id] && this.elemente[kind.id].fluss;
if (f && (kind === k || kind.elternWirksam === k)) {
const d = this.flussPfad(kind);
f.bett.setAttribute("d", d); f.hof.setAttribute("d", d); f.strom.setAttribute("d", d);
}
}
};
const ende = () => {
g.removeEventListener("pointermove", bewegen);
g.classList.remove("enf-zieht");
this.rahmenSetzen();
if (this.opt.onVerschoben) this.opt.onVerschoben(k.id, k.lage[0], k.lage[1]);
};
g.addEventListener("pointermove", bewegen);
g.addEventListener("pointerup", ende, { once: true });
g.addEventListener("pointercancel", ende, { once: true });
});
}
}
window.Energiefluss = Energiefluss;
})();