263 lines
12 KiB
JavaScript
263 lines
12 KiB
JavaScript
/* 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; }
|
|||
|
|
};
|
|||
|
|
})();
|