Files
erikandClaude Fable 5.1 2b59cedb25 Erfassung Eingriffs- und Kompensationsbilanz Sachsen (Prototyp)
Web-Erfassung für das Biotopwertverfahren nach der Handlungsempfehlung
Sachsen 2009 in der Fassung der Anlagen zur Sächsischen
Kompensationsverordnung (Stand 01/2017): HTML, Bootstrap 5, jQuery,
bootstrap-select, OpenLayers 10 (EPSG:25833, GeoSN-WMS).

- Rechenkern für Formblätter F1–F14 (js/bilanz.js)
- Karte mit Zeichenwerkzeugen, Wirkzonen, GeoJSON-Export (js/karte.js)
- Biotopwertliste Anlage 2 als JSON, per pdfplumber aus der PDF
  extrahiert (tools/parse_biotopwertliste.py), Faktoren der Anlagen
  1, 3, 4, 6.x (data/faktoren.json)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 09:32:02 +02:00

263 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
/* 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; }
};
})();