/* bilanz.js – Rechenkern für das Biotopwertverfahren Sachsen * Grundlage: Anlagen 2, 3, 4, 6.x und 7 (Formblätter F1–F14) der „Grundlagen für die Anlagen * zur Sächsischen Kompensationsverordnung“ (Stand 01/2017). * Reine Funktionen ohne DOM-Zugriff; wird von app.js benutzt. */ window.BIL = (function () { let BWL = null, FK = null, byBtc = {}; function init(bwl, fk) { BWL = bwl; FK = fk; byBtc = {}; bwl.typen.forEach(t => { byBtc[t.btc] = t; }); } const num = v => { const n = parseFloat(String(v ?? '').replace(',', '.')); return isFinite(n) ? n : 0; }; const r2 = x => Math.round((x + Number.EPSILON) * 100) / 100; const clamp = (v, a, b) => Math.min(b, Math.max(a, v)); const isEmpty = v => v === null || v === undefined || v === ''; function aasSumme(aas) { return (aas || []).reduce((s, a) => s + num(a.delta), 0); } /** effektiver Wert (bw|pw) eines Biotoptyps inkl. Auf-/Abschläge, geklemmt auf 0..30 */ function wert(btc, art, aas) { const t = byBtc[btc]; if (!t || t[art] === null || t[art] === undefined) return null; return clamp(t[art] + aasSumme(aas), FK.wp_min, FK.wp_max); } function stufe(w) { if (w === null || w === undefined) return ''; const k = FK.bedeutungsklassen.find(k => w >= k.von && w <= k.bis); return k ? k.stufe : ''; } const fmf = (bsf, afb) => (FK.tab41.FMF[bsf] || {})[afb] ?? null; const faf = (bsf, afb) => (FK.tab41.FAF[bsf] || {})[afb] ?? null; const isMinderung = afb => afb === 'FV' || afb === 'FS'; /** Faktor einer Funktionszeile: Override oder Tab. 4-1 */ function faktorZeile(k) { if (!isEmpty(k.faktor_override)) return num(k.faktor_override); return isMinderung(k.afb) ? fmf(k.bsf, k.afb) : faf(k.bsf, k.afb); } /* ---------------------------------------------------------------- F1 */ function calcF1(P) { const E = P.eingriff; const bestand = [], planung = []; let A1 = 0, P1 = 0; E.flaechen.forEach(f => { const g = num(f.gbf); const bw = wert(f.bt_bestand, 'bw', f.aas_bestand); const pw = wert(f.bt_planung, 'pw', f.aas_planung); const wpB = bw === null ? 0 : g * bw; const wpP = pw === null ? 0 : g * pw; A1 += wpB; P1 += wpP; bestand.push({ id: f.id, bez: f.bez, btc: f.bt_bestand, bt: byBtc[f.bt_bestand], aas: f.aas_bestand, wert: bw, gbf: g, wp: wpB }); planung.push({ id: f.id, bez: f.bez, btc: f.bt_planung, bt: byBtc[f.bt_planung], aas: f.aas_planung, wert: pw, gbf: g, wp: wpP }); }); let A2 = 0, P2 = 0; const fkRows = []; E.funktionen.forEach(k => { const g = num(k.gbf); const fak = faktorZeile(k); const wp = g * (fak || 0); const seite = isMinderung(k.afb) ? 'A' : 'P'; if (seite === 'A') A2 += wp; else P2 += wp; fkRows.push(Object.assign({}, k, { gbf: g, faktor: fak, wp, seite })); }); const land = calcLandschaft(E.landschaft || {}); const A3 = land.A3, P3 = land.P3; const B1 = (A1 + A2) - (P1 + P2); const B2 = A3 - P3; return { bestand, planung, fkRows, land, A1, A2, A3, P1, P2, P3, B1, B2 }; } /* ------------------------------------------------- Landschaftsbild 6.2 */ function calcLandschaft(L) { const LB = FK.landschaftsbild; const typ = LB.eingriffstypen.find(t => t.key === (L.typ || 'keiner')) || LB.eingriffstypen[0]; const res = { typ, zonen: [], A3: 0, P3: 0, aufwertungen: [], ersatz: null, radius1: null, radius2: null }; const addAufw = () => { (L.aufwertungen || []).forEach(a => { const f = !isEmpty(a.faktor_override) ? num(a.faktor_override) : faf(a.bsf, a.afb); const wp = num(a.gbf) * (f || 0); res.aufwertungen.push(Object.assign({}, a, { faktor: f, wp })); res.P3 += wp; }); }; if (typ.key === 'keiner') { addAufw(); return res; } const h = num(L.hoehe); let r1 = num(L.radius1) || typ.radius1 || 200; let r2 = typ.zone2 ? clamp(LB.radius_faktor_hoehe * h, 0, LB.radius_max) : 0; if (!isEmpty(L.radius2_override)) r2 = num(L.radius2_override); res.radius1 = r1; res.radius2 = r2; if (typ.key === 'mast_ab50') { res.ersatz = calcF12(L); addAufw(); return res; } const fwz1 = L.fwz1_manuell ? num(L.fwz1) : Math.PI * r1 * r1; const fwz2 = typ.zone2 ? (L.fwz2_manuell ? num(L.fwz2) : Math.max(0, Math.PI * r2 * r2 - Math.PI * r1 * r1)) : 0; const zonen = [ { nr: 1, name: 'Wirkzone 1 – Nahzone (bis ' + Math.round(r1) + ' m)', fwz: fwz1, wpk: typ.wpk1 }, { nr: 2, name: 'Wirkzone 2 – Mittelzone (' + Math.round(r1) + ' m bis ' + Math.round(r2) + ' m)', fwz: fwz2, wpk: typ.wpk2 } ]; zonen.forEach(z => { z.stufen = []; ['sehr hoch', 'hoch', 'mittel'].forEach(s => { const pct = num(((L.anteile || {})[z.nr] || {})[s]); const afe = z.fwz * pct / 100; const f = LB.fmf[s]; const wp = z.wpk * afe * f; z.stufen.push({ stufe: s, pct, afe, fmf: f, wp }); res.A3 += wp; }); }); res.zonen = typ.zone2 ? zonen : [zonen[0]]; addAufw(); return res; } /* ------------------------------------------------------------- F12 */ function calcF12(L) { const LB = FK.landschaftsbild; const bkg = num(L.baukosten); const rows = []; let summe = 0, ablSumme = 0; LB.stufen_ersatzzahlung.forEach(s => { const abl = num((L.abl || {})[s]); const proz = LB.ersatzzahlung_prozent[s]; const fks = bkg * proz / 100; const esz = abl * fks / 100; rows.push({ stufe: s, abl, proz, fks, esz }); summe += esz; ablSumme += abl; }); const rep = LB.repowering.find(r => r.key === (L.repowering || 'keins')) || LB.repowering[0]; const korr = summe * rep.faktor; return { rows, summe, ablSumme, rep, korr, E1: Math.round(summe + korr) }; } /* ------------------------------------------------------ Maßnahmen */ function calcAF(p) { // Anlage 6.5 Kat. 3: anzurechnende Fläche im erweiterten Einwirkungsbereich if (p.gewaesser) { return (num(p.bq) + 20) * Math.min(num(p.hs), 4000) * num(p.p) / 100; } const lkbMax = p.grossraeumig ? 500 : 250; const llz = p.grossraeumig ? Math.min(num(p.llz), 2500) : num(p.llz); return (num(p.bq) + llz) * Math.min(num(p.lkb), lkbMax) * num(p.p) / 100; } function calcMassnahme(m) { const p = m.params || {}; const warn = []; const typ = m.typ || 'F2'; const g = num(m.gbf); const tZ = byBtc[m.bt_ziel]; let bw = wert(m.bt_ausgang, 'bw', m.aas_ausgang); let pw = wert(m.bt_ziel, 'pw', m.aas_ziel); let grm = null; if (typ === 'F8') { grm = FK.fliessgewaesser.massnahmen.find(x => x.key === p.grm) || null; if (grm && grm.plus_wp) pw = bw === null ? null : clamp(bw + grm.plus_wp, FK.wp_min, FK.wp_max); } if (m.bt_ziel && tZ && (tZ.pw === null || tZ.pw === undefined) && !(grm && grm.plus_wp)) warn.push('Zielbiotoptyp hat keinen Planungswert (Kategorie der Ausgleichbarkeit D).'); const diff = (bw !== null && pw !== null) ? (pw - bw) : 0; if (bw !== null && pw !== null && diff < FK.signifikanz_min_wp && !['F5', 'F7'].includes(typ) && !(grm && grm.plus_wp)) { warn.push('Aufwertung nach Biotoptypen < ' + FK.signifikanz_min_wp + ' WP/m² – Signifikanzkriterium (Anlage 6 Nr. 2) nur über Aufwertung von Funktionen erfüllbar.'); } let S1 = 0, bonus = null, bonusName = '', bezugsflaeche = g, af = null; switch (typ) { case 'F2': S1 = diff * g; break; case 'F3': { const k = FK.entsiegelung.kbh.find(x => x.kbh == num(p.kbh)) || FK.entsiegelung.kbh[0]; bonus = k.bfe; bonusName = 'BFE'; S1 = diff * g * bonus; if (g > 0 && g < FK.entsiegelung.mindestflaeche_m2) warn.push('Mindestgröße der Entsiegelungsfläche (' + FK.entsiegelung.mindestflaeche_m2 + ' m²) unterschritten.'); break; } case 'F4': { const pik = FK.pik.massnahmen.find(x => x.key === p.pik) || { bfp: 1, zusatz: false }; const bfp = !isEmpty(p.bfp_override) ? num(p.bfp_override) : pik.bfp; const zfk = (p.zfk && pik.zusatz) ? FK.pik.zusatz_faktor : 0; const zfz = (p.zfz && pik.zusatz) ? FK.pik.zusatz_faktor : 0; bonus = bfp + zfk + zfz; bonusName = 'BFP' + (zfk ? '+ZFK' : '') + (zfz ? '+ZFZ' : ''); S1 = diff * g * bonus; break; } case 'F5': { const pik = FK.pik.massnahmen.find(x => x.key === p.pik) || { bfp: 1 }; const zfk = p.zfk ? FK.pik.zusatz_faktor : 0; bonus = pik.bfp + zfk; bonusName = 'BFP' + (zfk ? '+ZFK' : ''); if (pik.flaeche_je_einheit && !isEmpty(p.einheiten)) bezugsflaeche = num(p.einheiten) / 2 * pik.flaeche_je_einheit; S1 = bezugsflaeche * bonus; break; } case 'F6': { const kat = FK.wiedervernetzung.kategorien.find(x => x.key === (p.kwv || '1')) || FK.wiedervernetzung.kategorien[0]; bonus = kat.bfv[p.form || 'flaechig']; bonusName = 'BFV'; S1 = diff * g * bonus; break; } case 'F7': { const t = FK.wiedervernetzung.technisch; af = p.af_manuell ? num(p.af) : calcAF(p); S1 = num(p.gbf_ue) * t.bfv_ue + af * t.bfv_ee; bonusName = 'BFV 1,8 (UE) / 1,4 (EE)'; break; } case 'F8': { const useBonus = grm && grm.bonus && p.bonus !== false; bonus = useBonus ? FK.fliessgewaesser.bfr : null; bonusName = useBonus ? 'BFR' : ''; S1 = diff * g * (useBonus ? FK.fliessgewaesser.bfr : 1); break; } case 'F9': { bonus = num(p.bfn) || 3; bonusName = 'BFN (× Fläche)'; S1 = diff * g + g * bonus; break; } case 'F10': { const w = FK.weitere.massnahmen.find(x => x.key === p.art) || FK.weitere.massnahmen[0]; bonus = !isEmpty(p.bfn) ? num(p.bfn) : w.bfn; if (bonus > w.bfn_max) warn.push('Bonusfaktor über dem zulässigen Höchstwert ' + w.bfn_max + '.'); bonusName = 'BFN' + (w.bezug === 'flaeche' ? ' (× Fläche)' : ''); S1 = w.bezug === 'flaeche' ? diff * g + g * bonus : diff * g * bonus; break; } case 'F11': { const gA = num(p.gbf_a), gP = num(p.gbf_p); S1 = (pw || 0) * gP - (bw || 0) * gA; bezugsflaeche = gP; break; } } let S2 = 0, S3 = 0, S4 = 0; const fRows = []; (m.funktionen || []).forEach(k => { const fak = faktorZeile(k); const wp = num(k.gbf) * (fak || 0); if (k.funktion === 'landschaft') S4 += wp; else if (isMinderung(k.afb)) S3 += wp; else S2 += wp; fRows.push(Object.assign({}, k, { faktor: fak, wp })); }); if (typ === 'F3' && fRows.some(r => r.funktion !== 'landschaft')) warn.push('Entsiegelung: außer der landschaftsästhetischen Funktion sind keine Funktionsaufwertungsfaktoren zulässig (Anlage 6.3).'); if (typ === 'F7' && S2 > 0) warn.push('Technische Wiedervernetzung: keine zusätzlichen Funktionsaufwertungsfaktoren (Anlage 6.5).'); if (['F4', 'F5'].includes(typ) && fRows.some(r => r.funktion !== 'landschaft')) warn.push('PIK: Funktionsaufwertungsfaktoren nur für die landschaftsästhetische Funktion zulässig (Anlage 6.4).'); if (typ === 'F11' && S4 > 0 && fRows.filter(r => r.funktion === 'landschaft').some(r => (r.faktor || 0) < FK.trockenmauer.faf_landschaft_min)) warn.push('Trockenmauer: landschaftsästhetische Funktion mit mind. FAF ' + FK.trockenmauer.faf_landschaft_min + ' ansetzen.'); const L1 = S1 + S2 - S3, L2 = S4; return { typ, bw, pw, diff, g, bezugsflaeche, af, S1, S2, S3, S4, L1, L2, bonus, bonusName, fRows, warn, grm }; } function calcGesamt(P) { const f1 = calcF1(P); const ms = (P.massnahmen || []).map(m => ({ m, c: calcMassnahme(m) })); const L1 = ms.reduce((s, x) => s + x.c.L1, 0); const L2 = ms.reduce((s, x) => s + x.c.L2, 0); const mast = f1.land.typ.key === 'mast_ab50'; return { f1, ms, L1, L2, B1: f1.B1, B2: f1.B2, bil1: f1.B1 - L1, bil2: f1.B2 - L2, mast, E1: mast && f1.land.ersatz ? f1.land.ersatz.E1 : null }; } return { init, num, r2, clamp, isEmpty, wert, aasSumme, stufe, fmf, faf, isMinderung, faktorZeile, calcF1, calcLandschaft, calcF12, calcMassnahme, calcGesamt, calcAF, get byBtc() { return byBtc; }, get FK() { return FK; }, get BWL() { return BWL; } }; })();