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>
This commit is contained in:
erik
2026-09-07 09:32:02 +02:00
co-authored by Claude Fable 5.1
commit 2b59cedb25
10 changed files with 14388 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
*.pyc
__pycache__/
he.pdf
he.txt
+60
View File
@@ -0,0 +1,60 @@
# Eingriffs- und Kompensationsbilanz Sachsen (Prototyp)
Web-Erfassung für das Biotopwertverfahren nach der *Handlungsempfehlung zur Bewertung und
Bilanzierung von Eingriffen im Freistaat Sachsen* (SMUL 2009) in der Fassung der
*Grundlagen für die Anlagen zur Sächsischen Kompensationsverordnung* (TU Dresden /
Froelich & Sporbeck, Stand 01/2017). Reines Frontend: HTML + Bootstrap 5 + jQuery +
bootstrap-select + OpenLayers 10 (EPSG:25833 via proj4).
## Start
Die Seite lädt JSON-Daten per `fetch`, daher über einen Webserver aufrufen:
```bash
python3 -m http.server 8131 --directory /Users/erik/GIT/eingriffsbilanz
```
→ <http://localhost:8131>. Im Claude-Desktop ist die Konfiguration `eingriffsbilanz` in
`.claude/launch.json` der Wissensdatenbank hinterlegt.
## Funktionen
* **Karte**: OSM, Luftbild (GeoSN DOP-RGB), WebAtlasSN, ALKIS-Flurstücke (WMS, offene Daten),
Ortssuche (Nominatim). Flächen zeichnen/bearbeiten/löschen, geodätische Flächenberechnung,
Wirkzonen-Kreise für das Landschaftsbild, Verknüpfung Fläche ↔ Eintrag.
* **F1 Kompensationsbedarf**: Biotoptypen Bestand/Planung (A1/P1), Werte und Funktionen
besonderer Bedeutung mit FMF/FAF nach Tab. 4-1 (A2/P2), Landschaftsbild nach Anlage 6.2
(A3/P3) inkl. Ersatzzahlung F12 bei Masten ab 50 m.
* **F2–F11 Kompensationsleistung**: allgemeine Maßnahmen, Entsiegelung (BFE), PIK 1–10 (BFP,
ZFK/ZFZ), Wiedervernetzung nichttechnisch/technisch (BFV, aF-Formeln), Fließgewässer (BFR und
Steckbrief-Faktoren), Wald (BFN), weitere Maßnahmen (Moor, Stillgewässer, Offenland, Komplex),
Trockenmauer (F11), Baumpflanzung nach Anlage 6.1.
* **Auf-/Abschläge** (Spalte 12 der Biotopwertliste) je Fläche, nur zulässige Nummern.
* **F13/F14 Gesamtbilanz**, Formblätter druckbar, Speichern/Laden als JSON, GeoJSON-Export
(EPSG:25833) mit Bilanzattributen, Autosave im Browser (localStorage).
## Dateien
| Pfad | Inhalt |
|---|---|
| `index.html`, `css/app.css` | Oberfläche |
| `js/bilanz.js` | Rechenkern (reine Funktionen, Formblätter F1–F14) |
| `js/karte.js` | OpenLayers-Karte, WMS-Dienste, Zeichenwerkzeuge |
| `js/app.js` | Zustand, Formulare, Formblatt-Rendering, Persistenz |
| `data/biotopwertliste.json` | Anlage 2 – 527 Einträge (382 bewertbare Biotoptypen) mit BW, PW, KAG, Schutz, LRT, RL, AAS, Definitionen der *-Typen |
| `data/faktoren.json` | Anlagen 1, 3, 4, 6.x: Faktoren, Bonusfaktoren, PIK-Steckbriefe, GRM-Maßnahmen, AAS-Katalog, Einfachfälle |
| `tools/parse_biotopwertliste.py` | pdfplumber-Extraktion der Biotopwertliste aus der Quell-PDF (`he.pdf` = Handlungsempfehlung_kombiniert.pdf) |
## Datenqualität
Die Biotopwertliste wurde maschinell aus der PDF extrahiert und gegen die Textextraktion
validiert (alle 527 BTC gefunden). Fußnoten der Quelle (altersabhängige KAG bei Gehölzen,
Natürlichkeit bei Fels/Trockenrasen) sind nur als Alternativwerte (`bw_alt`, `pw_alt`) bzw.
Mehrfach-KAG (`C1/B1/A1`) abgebildet. Im Zweifel gilt die Quelle.
## Offene Punkte
* Flurstückssuche über den ALKIS-WFS, Übernahme von Flurstücksgeometrien als Fläche.
* Verschneidung Eingriffsfläche × Biotoptypenkartierung (BTLNK-WFS) zur automatischen Aufteilung.
* Serverseitige Ablage (VLN-Manager) statt localStorage; Mehrbenutzer.
* Word/PDF-Export der Formblätter über das bestehende Vorlagen-Modul.
+58
View File
@@ -0,0 +1,58 @@
html, body { height: 100%; }
body { display: flex; flex-direction: column; overflow: hidden; font-size: .9rem; background: #f4f5f7; }
.app-nav { flex: 0 0 auto; }
.app-body { flex: 1 1 auto; display: flex; min-height: 0; }
.map-col { flex: 1 1 58%; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid #ccc; background: #fff; position: relative; }
.panel-col { flex: 1 1 42%; display: flex; flex-direction: column; min-width: 420px; max-width: 760px; background: #fff; }
#map { flex: 1 1 auto; min-height: 0; background: #e9ecef; }
.map-toolbar { background: #f8f9fa; border-bottom: 1px solid #dee2e6; }
.map-status { background: #f8f9fa; border-top: 1px solid #dee2e6; }
.map-legend { position: absolute; right: 10px; top: 46px; background: rgba(255,255,255,.9); border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; display: flex; flex-direction: column; gap: 2px; pointer-events: none; }
.map-legend i.lg { display: inline-block; width: 14px; height: 10px; margin-right: 5px; border: 2px solid; vertical-align: middle; }
.lg-eingriff { border-color: #d62728 !important; background: rgba(214,39,40,.25); }
.lg-komp { border-color: #2ca02c !important; background: rgba(44,160,44,.25); }
.lg-funk { border-color: #ff7f0e !important; background: rgba(255,127,14,.2); border-style: dashed !important; }
.lg-land { border-color: #9467bd !important; background: rgba(148,103,189,.15); }
.lg-baum { border-color: #17becf !important; background: rgba(23,190,207,.3); border-radius: 50%; }
.panel-scroll { flex: 1 1 auto; overflow-y: auto; min-height: 0; }
.nav-tabs .nav-link { padding: .4rem .5rem; }
.card-ef { border-left: 4px solid #d62728; }
.card-fk { border-left: 4px solid #ff7f0e; }
.card-m { border-left: 4px solid #2ca02c; }
.card.selected { box-shadow: 0 0 0 2px #0d6efd; }
.card .card-body { padding: .5rem .6rem; }
.card .form-label { margin-bottom: 0; font-size: .75rem; color: #6c757d; }
.card .form-control-sm, .card .form-select-sm { font-size: .8rem; }
.bootstrap-select .dropdown-toggle { font-size: .8rem; padding: .25rem .5rem; }
.bootstrap-select .dropdown-menu { font-size: .8rem; }
.bootstrap-select .dropdown-menu li a span.text { white-space: normal; }
.bootstrap-select .dropdown-menu .dropdown-header { font-weight: 600; color: #495057; }
.bootstrap-select .filter-option-inner-inner { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wp { font-variant-numeric: tabular-nums; text-align: right; }
.wp-big { font-size: 1.3rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.wert-badge { font-variant-numeric: tabular-nums; min-width: 3.6rem; display: inline-block; text-align: center; }
.btn-xs { --bs-btn-padding-y: .1rem; --bs-btn-padding-x: .35rem; --bs-btn-font-size: .72rem; }
.fk-row, .faf-row { border-top: 1px dashed #dee2e6; padding-top: .35rem; margin-top: .35rem; }
.badge-typ { font-weight: 500; }
.formblaetter table { font-size: .75rem; width: 100%; margin-bottom: .75rem; }
.formblaetter th, .formblaetter td { padding: .15rem .3rem; border: 1px solid #adb5bd; vertical-align: top; }
.formblaetter th { background: #e9ecef; }
.formblaetter td.num, .formblaetter th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.formblaetter tr.sum td { font-weight: 600; background: #f8f9fa; }
.formblaetter h5 { margin-top: 1.2rem; font-size: 1rem; border-bottom: 2px solid #343a40; padding-bottom: .2rem; }
.formblaetter .fb-kopf { font-size: .8rem; color: #495057; }
.warn { color: #b02a37; }
.ok { color: #198754; }
.ol-scale-line { background: rgba(255,255,255,.7); bottom: 8px; left: 8px; }
.ol-scale-line-inner { color: #333; border-color: #333; }
#map.drawing { cursor: crosshair; }
@media print {
body { overflow: visible; display: block; background: #fff; }
.app-nav, .map-col, .nav-tabs, .toast-container, .modal { display: none !important; }
.app-body { display: block; }
.panel-col { max-width: none; min-width: 0; display: block; }
.panel-scroll { overflow: visible; }
.tab-pane { display: none !important; }
#tab-form { display: block !important; opacity: 1; }
.formblaetter h5 { page-break-before: auto; }
}
File diff suppressed because it is too large Load Diff
+292
View File
@@ -0,0 +1,292 @@
{
"stand": "01/2017",
"quelle": "Grundlagen für die Anlagen zur Sächsischen Kompensationsverordnung (TU Dresden / Froelich & Sporbeck, Stand 25.01.2017) – Anlagen 1, 3, 4, 6, 6.1–6.8, 7",
"wp_min": 0,
"wp_max": 30,
"aas_max_abs": 5,
"signifikanz_min_wp": 3,
"ausgleich_jahre": 25,
"bedeutungsklassen": [
{"von": 0, "bis": 6, "stufe": "sehr gering"},
{"von": 7, "bis": 12, "stufe": "gering"},
{"von": 13, "bis": 18, "stufe": "mittel"},
{"von": 19, "bis": 24, "stufe": "hoch"},
{"von": 25, "bis": 30, "stufe": "sehr hoch"}
],
"kag": {
"A1": "kurzfristig wiederherstellbar (≤ 5 Jahre), Wiederherstellung sicher",
"A2": "kurzfristig wiederherstellbar (≤ 5 Jahre), geringe Risiken",
"B1": "mittelfristig (5–25 Jahre), mittlere Risiken, standortunabhängig",
"B2": "mittelfristig (5–25 Jahre), mittlere Risiken, an Standorteigenschaften gekoppelt",
"C1": "nur langfristig (> 25 Jahre, nicht ausgleichbar), hohe Risiken, standortunabhängig",
"C2": "nur langfristig (> 25 Jahre, nicht ausgleichbar), hohe Risiken, standortgebunden",
"D": "auch langfristig nicht (mehr) erreichbar oder nicht erwünscht – kein Planungswert"
},
"bewertungsstufen": ["mittel", "hoch", "sehr hoch"],
"afb": {
"FV": "Funktionsverlust",
"FS": "Funktionssenkung",
"FH": "Funktionsherstellung",
"FE": "Funktionserhöhung"
},
"tab41": {
"FMF": {
"mittel": {"FV": 1.0, "FS": 0.5},
"hoch": {"FV": 1.5, "FS": 1.0},
"sehr hoch": {"FV": 2.0, "FS": 1.5}
},
"FAF": {
"mittel": {"FH": 0.9, "FE": 0.5},
"hoch": {"FH": 1.4, "FE": 0.9},
"sehr hoch": {"FH": 1.8, "FE": 1.4}
}
},
"ewb": {"UE": "unmittelbarer Einwirkungsbereich", "EE": "erweiterter Einwirkungsbereich"},
"funktionen": [
{"key": "spez_lebensraum", "name": "Spezifische Lebensraumfunktion", "schutzgut": "Arten und Biotope", "ewb": ["UE", "EE"],
"kurz": "sehr hoch: Kernbestandteile des Lebensraums vom Aussterben bedrohter/stark gefährdeter Arten, Anhang-IV-/streng geschützter Vogel-/prioritärer Arten, FFH-LRT · hoch: Kernlebensraum mehrerer gefährdeter Arten bzw. einer gefährdeten, besonders geschützten Vogelart mit hervorgehobener Bedeutung · mittel: Kernlebensraum einer gefährdeten Art"},
{"key": "biotopverbund", "name": "Biotopverbundfunktion", "schutzgut": "Arten und Biotope", "ewb": ["UE"],
"kurz": "sehr hoch: Kernflächen/bundes- oder landesweit bedeutsame Verbindungsflächen des Biotopverbunds · hoch: regional bedeutsame Verbindungsflächen/Trittsteine · mittel: örtlich bedeutsame Verbindungsflächen (kommunaler Landschaftsplan)"},
{"key": "biotische_standort", "name": "Biotische Standortfunktion", "schutzgut": "Boden", "ewb": ["UE"],
"kurz": "Natürliche Bodenfruchtbarkeit: sehr hoch: Acker-/Grünlandzahl > 70, Vorranggebiet Landwirtschaft · hoch: > 50–70, Vorbehaltsgebiet · mittel: > 35–50"},
{"key": "boden_archiv", "name": "Natürliche Boden- und Archivfunktionen", "schutzgut": "Boden", "ewb": ["UE", "EE"],
"kurz": "sehr hoch: sehr hohes Biotopentwicklungspotenzial (nass/sehr trocken + nährstoffarm), sehr naturnahe Böden (Moor, > 150 J. Wald), archäolog. Kulturdenkmale, Geotope · hoch: hohes Biotopentwicklungspotenzial, landschaftsgeschichtlich bedeutsame/seltene Böden · mittel: mittleres Entwicklungspotenzial, geringes Filter-/Puffervermögen"},
{"key": "gw_neubildung", "name": "Grundwasserneubildungsfunktion", "schutzgut": "Wasser", "ewb": ["UE"],
"kurz": "Grundwasserneubildungsrate: sehr hoch > 250 mm/a · hoch 150–250 mm/a · mittel 100–150 mm/a"},
{"key": "retention", "name": "Retentionsfunktion", "schutzgut": "Wasser", "ewb": ["UE"],
"kurz": "sehr hoch: festgesetzte Überschwemmungsgebiete (HQ100), Polder, Vorranggebiete Hochwasserschutz · hoch: überschwemmungsgefährdete Gebiete, Auenböden, Vorbehaltsgebiete · mittel: Hochwasserentstehungsgebiete, Bereiche mit besonderen Nutzungsanforderungen (Wasserrückhalt)"},
{"key": "bioklima", "name": "Bioklimatische Ausgleichsfunktion", "schutzgut": "Klima / Luft", "ewb": ["UE"],
"kurz": "sehr hoch: überregional bedeutsame siedlungsrelevante Frisch-/Kaltluftbahnen · hoch: regional bedeutsame Bahnen, Kaltluftentstehungsgebiete für stark überwärmte Wirkräume, Komfortinseln > 1 ha · mittel: örtliche Bahnen, Komfortinseln 0,5–1 ha"},
{"key": "luftregeneration", "name": "Luftregenerations- und Klimaschutzfunktion", "schutzgut": "Klima / Luft", "ewb": ["UE"],
"kurz": "sehr hoch: zusammenhängende Wälder > 100 ha in Hauptwindrichtung eines lufthygienisch besonders belasteten Wirkraums, intakte Moore · hoch: Wälder im belasteten Wirkraum, hydromorphe Böden mit hpnV-Bestockung · mittel: Gehölzbestände im belasteten Wirkraum"}
],
"landschaftsbild": {
"eingriffstypen": [
{"key": "keiner", "name": "keine erhebliche Beeinträchtigung / nicht betroffen"},
{"key": "sonstige", "name": "sonstiger Eingriff (sonstige bauliche Anlage)", "wpk1": 0.1, "wpk2": 0.1, "radius1": 200, "radius1_min": 50, "zone2": false},
{"key": "mast_10_50", "name": "mastenartiger Eingriff 10 m bis unter 50 m Gesamthöhe", "wpk1": 0.2, "wpk2": 0.1, "radius1": 200, "zone2": true},
{"key": "mast_ab50", "name": "mastenartiger Eingriff ab 50 m Gesamthöhe → Ersatzzahlung (F12)", "zone2": true}
],
"radius_faktor_hoehe": 25,
"radius_max": 10000,
"fmf": {"sehr hoch": 1.5, "hoch": 1.0, "mittel": 0.5},
"stufen_ersatzzahlung": ["sehr hoch", "hoch", "mittel", "gering", "sehr gering"],
"ersatzzahlung_prozent": {"sehr hoch": 10, "hoch": 5, "mittel": 3, "gering": 1, "sehr gering": 0},
"repowering": [
{"key": "keins", "name": "kein Repowering", "faktor": 0},
{"key": "vreg", "name": "Repowering innerhalb Vorrang-/Eignungsgebiet (−10 %)", "faktor": -0.10},
{"key": "vreg_rueckbau", "name": "Repowering innerhalb VREG mit vollständigem Rückbau außerhalb VREG (−15 %)", "faktor": -0.15},
{"key": "ausserhalb", "name": "Repowering außerhalb Vorrang-/Eignungsgebiet (+10 %)", "faktor": 0.10}
]
},
"aas": [
{"nr": 1, "titel": "Ausprägung Gehölzbestand (Gehölzbiotoptypen)", "optionen": [
{"key": "1a", "delta": -4, "text": "Gehölzbiotope außer Gebüsche, Mittel-/Strauchhecken, Wälder, Forsten: überwiegender BHD max. 7 cm (Obst max. 5 cm) oder Alter max. 5 Jahre"},
{"key": "1b", "delta": -2, "text": "Gehölzbiotope außer Gebüsche, Mittel-/Strauchhecken, Wälder, Forsten: überwiegender BHD max. 20 cm (Obst max. 10 cm) oder Alter max. 25 Jahre"},
{"key": "1c", "delta": -2, "text": "Mittel-/Strauchhecken (außer Baumhecken), Gebüsche, Vorwälder, Waldränder, Aufforstungsflächen: Alter max. 5 Jahre"},
{"key": "1d", "delta": 1, "text": "überwiegender BHD > 60 cm (Obstbäume > 30 cm)"},
{"key": "1e", "delta": 2, "text": "überwiegender BHD > 100 cm (Obstbäume > 40 cm)"},
{"key": "1f", "delta": 1, "text": "Mischforsten inkl. Aufforstungsflächen: Anteil heimischer Laubbaumarten > 50 %"}
]},
{"nr": 2, "titel": "Deckungsgrad Baumbestand", "optionen": [
{"key": "2a", "delta": 1, "text": "waldartiger Baumbestand mind. 30 % Deckung"},
{"key": "2b", "delta": 2, "text": "waldartiger Baumbestand mind. 50 % Deckung"}
]},
{"nr": 3, "titel": "Statusänderung durch spezifische Ausprägung", "optionen": [
{"key": "3a", "delta": 2, "text": "aufgrund der Ausprägung nach § 30 BNatSchG / § 21 SächsNatSchG geschütztes Biotop (nur bei (§)-Typen)"}
]},
{"nr": 4, "titel": "Fehlen kennzeichnender Arten", "optionen": [
{"key": "4a", "delta": -2, "text": "Biotoptyp/Untertyp weist keine kennzeichnenden Arten auf"}
]},
{"nr": 5, "titel": "Vorhandensein invasiver Neophyten", "optionen": [
{"key": "5a", "delta": -2, "text": "erhebliche Flächenanteile invasiver Arten (Sächs. Neobiota-Liste / Unionsliste) – geringe Ausprägung"},
{"key": "5b", "delta": -3, "text": "erhebliche Flächenanteile invasiver Arten – mittlere Ausprägung"},
{"key": "5c", "delta": -4, "text": "erhebliche Flächenanteile invasiver Arten – hohe Ausprägung"},
{"key": "5d", "delta": -5, "text": "erhebliche Flächenanteile invasiver Arten – sehr hohe Ausprägung (Maximum)"}
]},
{"nr": 6, "titel": "Verbuschungsgrad", "optionen": [
{"key": "6a", "delta": -2, "text": "Verbuschungsgrad > 25 %"}
]},
{"nr": 7, "titel": "Vorhandensein von Verlandungsbereichen", "optionen": [
{"key": "7a", "delta": 2, "text": "Vorkommen naturnaher Verlandungsvegetation"}
]},
{"nr": 8, "titel": "Natur-/landschaftsunverträgliche Nutzung", "optionen": [
{"key": "8a", "delta": -2, "text": "natur-/landschaftsunverträgliche Erholungsnutzung"}
]},
{"nr": 9, "titel": "Anteil an Kulturarten", "optionen": [
{"key": "9a", "delta": -2, "text": "Anteil Kulturarten > 40 %"}
]},
{"nr": 10, "titel": "Naturschutzrelevante Standorteigenschaften", "optionen": [
{"key": "10a", "delta": 2, "text": "spezifische, naturschutzrelevante Standorteigenschaften (feucht/nass, trocken, mager)"}
]},
{"nr": 11, "titel": "Abstand von Straßen", "optionen": [
{"key": "11a", "delta": -4, "text": "bis 25 m beidseitig von Straßen > 10.000 Kfz/24 h (bzw. Autobahnen/Bundesstraßen ohne Zählung)"},
{"key": "11b", "delta": -2, "text": "bis 25 m beidseitig von Straßen 5.000–10.000 Kfz/24 h (bzw. Staatsstraßen) oder 25–50 m von Straßen > 10.000 Kfz/24 h"}
]},
{"nr": 12, "titel": "Sohlgestaltung Stillgewässer", "optionen": [
{"key": "12a", "delta": 1, "text": "wasserdurchlässige Befestigung der Sohle"}
]},
{"nr": 13, "titel": "Oberflächenbefestigung", "optionen": [
{"key": "13a", "delta": 1, "text": "ungebundene Befestigung von Wegen/Plätzen durch wassergebundene Wegedecken"},
{"key": "13b", "delta": 2, "text": "Plätze (≥ 100 m²) und Wege (ab 10 m Breite): Dränsteine, Sickerfugen-/Rasengittersteine, Pflaster/Platten in ungebundener Bauweise"}
]},
{"nr": 14, "titel": "Verwendung gebietsfremder Arten", "optionen": [
{"key": "14a", "delta": -4, "text": "Verwendung von Saatgut/Gehölzen aus anderen Vorkommensgebieten"}
]},
{"nr": 15, "titel": "Hochwasserschutzdeich (Deichgrünland)", "gruppen": ["Oberboden", "Begrünung", "Pflege"], "optionen": [
{"key": "15a", "gruppe": "Oberboden", "delta": 1, "text": "Verwendung gebietsheimischen Oberbodens (lokale Herkunft, Eignung nach Entwicklungsziel)"},
{"key": "15b", "gruppe": "Oberboden", "delta": 2, "text": "Wiederverwendung Ausgangsoberboden / Übertrag gebietsheimisch-lokalen, diasporenreichen Substrats aus hochwertigen Vorzuständen"},
{"key": "15c", "gruppe": "Oberboden", "delta": 1, "text": "Substrat ausgehagert, nährstoffarm"},
{"key": "15d", "gruppe": "Begrünung", "delta": 2, "text": "gebietsheimische, kräuterreiche Saatgutmischung (z. B. RSM 8.1)"},
{"key": "15e", "gruppe": "Begrünung", "delta": 4, "text": "Mahdgutübertragung / Heumulch / Heudrusch / Sodenübertrag o. Vergleichbares"},
{"key": "15f", "gruppe": "Begrünung", "delta": 1, "text": "spezifischer Diasporenübertrag (z. B. lokal seltene Arten)"},
{"key": "15g", "gruppe": "Pflege", "delta": 1, "text": "Mindeststandard extensive Pflege (max. 2 Mahd-/Beweidungstermine, keine Düngung)"},
{"key": "15h", "gruppe": "Pflege", "delta": 2, "text": "erhöhter Standard extensive Pflege (Mahdgutentfernung, Pflegefrequenz, Beschränkung Nachsaat)"},
{"key": "15i", "gruppe": "Pflege", "delta": 3, "text": "zielspezifisches, dauerhaftes Pflege- und Entwicklungsregime im Einvernehmen mit der Naturschutzbehörde"}
]}
],
"einzelbaum": {
"ausgangs_btc": "02.02.470",
"kronentrauf_obst_m2": 20,
"kronentrauf_laub_m2": 30,
"hinweis": "Anlage 6.1: Bestand = Biotopwert × Kronentrauffläche (r²·π); Neupflanzung = Planungswert × pauschale Kronentrauffläche (Obst 20 m², Laub 30 m²), Ausgangsbiotoptyp 02.02.470 (0 WP)."
},
"entsiegelung": {
"mindestflaeche_m2": 50,
"kbh": [
{"kbh": 0, "name": "Entsiegelung ohne Hochbau / Bauwerkshöhe < 3 m", "bfe": 3.0},
{"kbh": 1, "name": "Abriss Hochbau 3 bis < 10 m", "bfe": 4.0},
{"kbh": 2, "name": "Abriss Hochbau 10 bis < 20 m", "bfe": 5.0},
{"kbh": 3, "name": "Abriss Hochbau ab 20 m", "bfe": 6.0}
],
"hinweis": "Anlage 6.3: (PW − BW) × GBF × BFE; Zielzustand nach 25 Jahren mind. Vorwald; keine zusätzlichen Funktionsaufwertungsfaktoren außer landschaftsästhetische Funktion."
},
"pik": {
"zusatz_faktor": 0.2,
"massnahmen": [
{"key": "1", "nr": "PIK 1", "name": "Etablierung von extensivem Ackerbau zum Ackerwildkrautschutz", "bfp": 1.7, "zusatz": true, "formblatt": "F4", "ausgang": ["10.01.200"], "ziel": ["10.01.110", "10.01.120", "10.01.130", "10.01.140", "10.01.150"]},
{"key": "2", "nr": "PIK 2", "name": "Anlage von Ackerrandstreifen", "bfp": 1.1, "zusatz": true, "formblatt": "F4", "ausgang": ["10.01.200"], "ziel": ["07.05.100"]},
{"key": "3", "nr": "PIK 3", "name": "Anlage von Einsaatblühstreifen", "bfp": 1.6, "zusatz": true, "formblatt": "F4", "ausgang": ["10.01.200"], "ziel": ["07.04.100"]},
{"key": "4a", "nr": "PIK 4", "name": "Anlage von Ackerbrachen (Selbstbegrünung)", "bfp": 1.2, "zusatz": true, "formblatt": "F4", "ausgang": ["10.01.200"], "ziel": ["10.01.410", "10.01.420", "10.01.430", "10.01.440", "10.01.450"]},
{"key": "4b", "nr": "PIK 4", "name": "Anlage von Ackerbrachen (Einsaatbrachen)", "bfp": 1.6, "zusatz": true, "formblatt": "F4", "ausgang": ["10.01.200"], "ziel": ["10.01.410", "10.01.420", "10.01.430", "10.01.440", "10.01.450"]},
{"key": "5", "nr": "PIK 5", "name": "Etablierung einer extensiven Grünlandnutzung", "bfp": 1.2, "zusatz": true, "formblatt": "F4", "ausgang": ["06.03.100", "06.03.210", "06.03.220"], "ziel": ["06.01.510", "06.02.210", "06.02.220"]},
{"key": "6", "nr": "PIK 6", "name": "Anlage von Altgrasstreifen", "bfp": 1.1, "zusatz": false, "formblatt": "F4", "ausgang": ["06.03.100", "06.03.210"], "ziel": ["07.06.100"]},
{"key": "7", "nr": "PIK 7", "name": "Etablierung ökologisch wertvoller Säume", "bfp": 1.4, "zusatz": true, "formblatt": "F4", "ausgang": ["06.03.100", "06.03.210", "06.03.220"], "ziel": ["07.01.120", "07.01.140", "07.01.210", "07.01.220", "07.01.310"]},
{"key": "8", "nr": "PIK 8", "name": "Etablierung von Grünlandsondernutzungsformen (degenerierte → intakte Biotoptypen)", "bfp": 3.0, "bfp_max": 6.0, "zusatz": false, "formblatt": "F4", "ausgang": ["05.04.130", "05.04.230", "05.04.320", "05.04.470", "06.01.120", "06.01.230", "06.01.320", "06.01.420", "06.02.140", "06.02.320", "08.01.500", "08.02.200", "08.03.200", "08.05.140", "08.05.250"], "ziel": []},
{"key": "9", "nr": "PIK 9", "name": "Etablierung von extensivem Weinbau", "bfp": 1.1, "zusatz": true, "formblatt": "F4", "ausgang": ["10.04.200"], "ziel": ["10.04.100"]},
{"key": "10a", "nr": "PIK 10", "name": "Artenschutz-PIK Feldlerchenfenster", "bfp": 1.1, "zusatz": true, "formblatt": "F5", "bezug": "1.000 m² pro zwei Lerchenfenster (Fläche = Fensterpaare × 1.000 m²)", "flaeche_je_einheit": 1000},
{"key": "10b", "nr": "PIK 10", "name": "Artenschutz-PIK Kiebitzinsel", "bfp": 1.6, "zusatz": true, "formblatt": "F5", "bezug": "Maßnahmenfläche der Kiebitzinseln (3.000–25.000 m² je Insel)"},
{"key": "10c", "nr": "PIK 10", "name": "Artenschutz-PIK Feldhamsterhabitat", "bfp": 1.6, "zusatz": true, "formblatt": "F5", "bezug": "Maßnahmenfläche (ganzer Ackerschlag)"}
]
},
"wiedervernetzung": {
"kategorien": [
{"key": "1", "name": "Kat. 1 – nichttechnische Maßnahme ohne spezifischen Zielartenbezug", "bfv": {"flaechig": 1.4, "trittstein": 1.2}},
{"key": "2", "name": "Kat. 2 – nichttechnische Maßnahme mit spezifischem Zielartenbezug", "bfv": {"flaechig": 1.8, "trittstein": 1.6}}
],
"formen": {"flaechig": "flächige Wiederherstellung des Biotopverbunds", "trittstein": "Wiederherstellung in Form von Trittsteinen"},
"technisch": {"bfv_ue": 1.8, "bfv_ee": 1.4,
"hinweis": "Kat. 3 (F7): Fläche der Querungsanlage × 1,8 (UE) + anzurechnende Fläche aF × 1,4 (EE). aF gewässergebunden = (BQ + 20) · hS · p; nicht gewässergebunden = (BQ + LLZ) · LKB · p. LKB max. 250 m (kleinräumig) / 500 m (großräumig), LLZ max. 2.500 m, hS max. 4.000 m."}
},
"fliessgewaesser": {
"bfr": 2.0,
"faf_biotopverbund_wiederherstellung": {"bach": 1.4, "fluss": 1.8},
"faf_biotopverbund_erhoehung": {"bach": 0.9, "fluss": 1.4},
"massnahmen": [
{"key": "1a", "name": "Komplettrückbau eines Querbauwerks", "bonus": true, "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Fläche Querbauwerkbereich", "bach": 1.4, "fluss": 1.8},
{"funktion": "landschaft", "afb": "FH", "bezug": "Fläche Querbauwerkbereich", "bach": 1.4, "fluss": 1.4},
{"funktion": "biotopverbund", "afb": "FE", "bezug": "Gewässerfläche bis zum nächsten Querbauwerk ober-/unterhalb (max. 4 km)", "bach": 0.9, "fluss": 1.4}]},
{"key": "1b", "name": "Komplettrückbau eines Querbauwerks mit Laufverlängerung", "bonus": true, "faf": [
{"funktion": "landschaft", "afb": "FH", "bezug": "Fläche ehem. Querbauwerkbereich", "bach": 1.4, "fluss": 1.4},
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Fläche des neuen Gewässerabschnitts", "bach": 1.4, "fluss": 1.8},
{"funktion": "biotopverbund", "afb": "FE", "bezug": "Gewässerfläche bis zum nächsten Querbauwerk (max. 4 km)", "bach": 0.9, "fluss": 1.4}]},
{"key": "1c", "name": "Umbau eines Querbauwerks in eine Sohlgleite", "bonus": true, "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Fläche der neuen Sohlgleite", "bach": 1.4, "fluss": 1.8},
{"funktion": "biotopverbund", "afb": "FE", "bezug": "Gewässerfläche bis zum nächsten Querbauwerk (max. 4 km)", "bach": 0.9, "fluss": 1.4}]},
{"key": "1d", "name": "Komplettrückbau eines Querbauwerks mit Auswirkungen auf Nebengewässer", "bonus": true, "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Fläche Querbauwerkbereich", "bach": 1.4, "fluss": 1.8},
{"funktion": "landschaft", "afb": "FH", "bezug": "Fläche Querbauwerkbereich", "bach": 1.4, "fluss": 1.4},
{"funktion": "biotopverbund", "afb": "FE", "bezug": "Haupt- und Nebengewässer bis zum nächsten Querbauwerk (max. 4 km)", "bach": 0.9, "fluss": 1.4}]},
{"key": "2a", "name": "Beseitigung der Rückstauwirkung bei Rückbau eines Querbauwerks", "bonus": true, "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Fläche ehem. Rückstaubereich", "bach": 1.4, "fluss": 1.8}]},
{"key": "2b", "name": "Verringerung der Rückstauwirkung bei Umbau eines Querbauwerks", "bonus": false, "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Fläche ehem. Rückstaubereich", "bach": 1.4, "fluss": 1.8}]},
{"key": "3a", "name": "Anlage von Umgehungsgerinnen", "bonus": false, "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Fläche Umgehungsgerinne", "bach": 1.4, "fluss": 1.8},
{"funktion": "biotopverbund", "afb": "FE", "bezug": "Gewässerfläche bis zum nächsten Querbauwerk (max. 4 km)", "bach": 0.9, "fluss": 1.4}]},
{"key": "4a", "name": "Vollständige Entfernung von Verrohrungen und Sohl-/Uferverbau", "bonus": true, "bonus_hinweis": "Bonus nur bei massiven (Beton-)Verbauungen, Verrohrungen oder außergewöhnlich aufwendigem Rückbau", "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Sohlbereich", "bach": 1.4, "fluss": 1.8},
{"funktion": "biotopverbund", "afb": "FE", "bezug": "Uferbereich", "bach": 0.9, "fluss": 1.4},
{"funktion": "landschaft", "afb": "FH", "bezug": "Rückbaufläche", "bach": 1.4, "fluss": 1.4}]},
{"key": "4b", "name": "Dauerhafte Einstellung der Gewässerunterhaltung (Ausgangsbiotoptyp + 3 WP/m²)", "bonus": false, "plus_wp": 3, "faf": []},
{"key": "4c", "name": "Einbringung von Strukturelementen (Ausgangsbiotoptyp + 3 WP/m²)", "bonus": false, "plus_wp": 3, "faf": []},
{"key": "5a", "name": "Anlage eines Gewässerentwicklungsraumes", "bonus": false, "faf": [
{"funktion": "biotopverbund", "afb": "FE", "bezug": "Gewässerentwicklungsraum", "bach": 0.9, "fluss": 1.4},
{"funktion": "retention", "afb": "FE", "bezug": "Gewässerentwicklungsraum", "bach": 1.4, "fluss": 1.4},
{"funktion": "boden_archiv", "afb": "FE", "bezug": "Gewässerentwicklungsraum (nur Bereiche ohne ausgeprägte Vegetation)", "bach": 0.9, "fluss": 0.9},
{"funktion": "landschaft", "afb": "FE", "bezug": "Gewässerentwicklungsraum", "bach": 0.9, "fluss": 0.9}]},
{"key": "6a", "name": "Anlage/Anbindung einer Flutrinne", "bonus": false, "faf": [
{"funktion": "retention", "afb": "FH", "bezug": "Fläche der Flutrinne", "bach": 1.8, "fluss": 1.8}]},
{"key": "6b", "name": "Anbindung eines Altwassers", "bonus": false, "faf": [
{"funktion": "biotopverbund", "afb": "FH", "bezug": "Anbindung + angebundenes Altwasser", "bach": 1.8, "fluss": 1.8},
{"funktion": "retention", "afb": "FH", "bezug": "Fläche der Anbindung", "bach": 1.8, "fluss": 1.8}]},
{"key": "7a", "name": "Verbesserung der Überflutungssituation – Reaktivierung Primäraue", "bonus": false, "faf": [
{"funktion": "biotopverbund", "afb": "FE", "bezug": "wieder überschwemmter Überflutungsraum", "bach": 1.4, "fluss": 1.4},
{"funktion": "retention", "afb": "FH", "bezug": "wieder überschwemmter Überflutungsraum", "bach": 1.8, "fluss": 1.8}]},
{"key": "7b", "name": "Verbesserung der Überflutungssituation – Anlage Sekundäraue", "bonus": false, "faf": [
{"funktion": "retention", "afb": "FE", "bezug": "Überflutungsraum der Sekundäraue", "bach": 1.4, "fluss": 1.4}]}
]
},
"wald": {
"massnahmen": [
{"key": "naturwaldzelle", "name": "Anlage von Naturwaldzellen (flächiger Nutzungsverzicht in naturnahen Altbeständen)", "bfn": 3.0, "bfn_optionen": [{"bfn": 3.0, "text": "Regelfall"}, {"bfn": 5.0, "text": "BHD der 100 stärksten Bäume/ha ≥ 50 cm ODER ≥ 5 Biotopbäume/ha"}, {"bfn": 6.0, "text": "beide Kriterien erfüllt"}], "ziel": ["01.11.200"], "faf_hinweis": "zulässig: Erhöhung eines Wertes sehr hoher Bedeutung der landschaftsästhetischen Funktion (FE, sehr hoch = 1,4)"},
{"key": "mittelwald", "name": "Einrichtung historischer Waldbewirtschaftungsform: Mittelwald", "bfn": 3.0, "ziel": ["01.12.200"], "faf_hinweis": "zulässig: Wiederherstellung eines Wertes sehr hoher Bedeutung der landschaftsästhetischen Funktion (FH, sehr hoch = 1,8)"},
{"key": "hutewald", "name": "Einrichtung historischer Waldbewirtschaftungsform: Hutewald", "bfn": 3.0, "ziel": ["01.12.100"], "faf_hinweis": "zulässig: Wiederherstellung eines Wertes sehr hoher Bedeutung der landschaftsästhetischen Funktion (FH, sehr hoch = 1,8)"}
],
"hinweis": "Anlage 6.7 / F9: Aufwertung = (PW − BW) × GBF + GBF × BFN (Bonus bezieht sich auf die Fläche). Mindestfläche 0,25 ha. Erstaufforstung, Waldumbau und Sukzession werden als allgemeine Maßnahmen (F2) bilanziert."
},
"weitere": {
"massnahmen": [
{"key": "moor", "name": "Renaturierung von Mooren", "bfn": 3.0, "bfn_max": 6.0, "bezug": "wp"},
{"key": "stillgewaesser", "name": "Wiederherstellung naturnaher Stillgewässer", "bfn": 2.0, "bfn_max": 2.0, "bezug": "wp"},
{"key": "offenland", "name": "Wiederherstellung durch Nutzungsauflassung degenerierter Offenlandbiotope", "bfn": 3.0, "bfn_max": 6.0, "bezug": "wp"},
{"key": "komplex", "name": "Sonstige Komplexmaßnahme besonderer naturschutzfachlicher Bedeutung (Ausnahme, UNB)", "bfn": 2.0, "bfn_max": 2.0, "bezug": "flaeche"}
],
"hinweis": "Anlage 6.8 / F10: Aufwertung = (PW − BW) × GBF × BFN (Bezug Wertpunkte). Komplexmaßnahmen: Fläche × BFN wird zur Differenz addiert."
},
"trockenmauer": {
"ausgang": "09.07.320",
"ziel": "09.07.310",
"faf_landschaft_min": 0.9,
"hinweis": "Anlage 6.8 Nr. 2 / F11: Bewertung aller sichtbaren Maueroberflächen (GBF Ausgang ≠ GBF Planung möglich); additiv Wiederherstellung der landschaftsästhetischen Funktion mit mind. FAF 0,9."
},
"massnahmentypen": [
{"key": "F2", "name": "Allgemeine Kompensationsmaßnahme (Biotopentwicklung, Erstaufforstung, Waldumbau, Sukzession, Baumpflanzung)", "kurz": "F2 Allgemein"},
{"key": "F3", "name": "Entsiegelung / Abriss (Anlage 6.3)", "kurz": "F3 Entsiegelung"},
{"key": "F4", "name": "Produktionsintegrierte Kompensation PIK 1–9 (Anlage 6.4)", "kurz": "F4 PIK 1–9"},
{"key": "F5", "name": "Artenschutz-PIK 10 (Anlage 6.4)", "kurz": "F5 PIK 10"},
{"key": "F6", "name": "Wiedervernetzung – nichttechnisch, Kat. 1/2 (Anlage 6.5)", "kurz": "F6 Wiedervernetzung"},
{"key": "F7", "name": "Wiedervernetzung – technisch, Kat. 3 (Anlage 6.5)", "kurz": "F7 Querungshilfe"},
{"key": "F8", "name": "Fließgewässer und Auen (Anlage 6.6)", "kurz": "F8 Fließgewässer"},
{"key": "F9", "name": "Spezifische Maßnahmen im Wald (Anlage 6.7)", "kurz": "F9 Wald"},
{"key": "F10", "name": "Weitere spezifische Maßnahmen: Moor, Stillgewässer, Offenland, Komplex (Anlage 6.8)", "kurz": "F10 Weitere"},
{"key": "F11", "name": "Maßnahme mit Abweichung in der Flächenbilanz, z. B. Trockenmauersanierung", "kurz": "F11 Trockenmauer"}
],
"einfachfaelle": [
"1.1 Bauliche Anlagen unter der Erdoberfläche im Außenbereich bis 50 m² (mind. 10 m³)",
"1.2 Nicht überdachte Stellplatzanlagen einschl. Zufahrten bis 100 m² Grundfläche",
"1.3 Landwirtschaftliche Bauvorhaben nach § 35 Abs. 1 Nr. 1–2 BauGB: Hallen/Lager/Ställe bis 5 m Traufhöhe und 100 m²; Flachsilos < 50 m², Hochsilos < 8 m bzw. 30 m³; Behälter bis 30 m²/3 m",
"1.4 Gewächshäuser bis 5 m Firsthöhe und 100 m²",
"1.5 Garagen bis 3 m mittlere Wandhöhe und 50 m²",
"2.1 Bauliche Anlagen der öffentlichen Ver-/Entsorgung bis 20 m² und 4 m Höhe (Trafo-, Pumpstationen …)",
"2.2 Ober-/unterirdische Leitungen im Außenbereich bis 500 m Länge ohne Baustraße",
"2.3 Unbefestigte Ausstellungs-/Lagerplätze bis 100 m² für land-/forstwirtschaftliche Erzeugnisse",
"2.4 Wanderparkplätze (ohne bauliche Anlagen) bis 100 m² im Außenbereich",
"2.5 Spiel- und Sportplätze (ohne bauliche Anlagen) bis 100 m² im Außenbereich",
"3.1 Waldumwandlung, sofern ausschließlich Waldersatz nach SächsWaldG resultiert",
"3.2 Werbeanlagen an der Stätte der Leistung und Wegweiser bis 3 m Höhe oder 10 m²",
"3.3 Einfriedungen im Innenbereich sowie offene Einfriedungen bis 2 m / geschlossene bis 1,5 m im Außenbereich",
"3.4 Baustelleneinrichtungen bis zum Abschluss der Bauarbeiten"
]
}
+240
View File
@@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Eingriffs- und Kompensationsbilanz Sachsen</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.14.0-beta3/css/bootstrap-select.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/openlayers/10.6.1/ol.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<nav class="navbar navbar-dark bg-dark navbar-expand px-3 py-1 app-nav">
<span class="navbar-brand mb-0 h1 fs-6"><i class="bi bi-tree-fill me-1"></i>Eingriffs- und Kompensationsbilanz <span class="text-secondary fw-normal">Sachsen · Biotopwertverfahren (Stand 01/2017)</span></span>
<div class="ms-auto d-flex align-items-center gap-2">
<span class="text-secondary small d-none d-lg-inline" id="autosave-info"></span>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-light" id="btn-neu" title="Neues Vorhaben (leert alle Eingaben)"><i class="bi bi-file-earmark-plus"></i> Neu</button>
<button class="btn btn-outline-light" id="btn-export" title="Vorhaben als JSON-Datei speichern"><i class="bi bi-download"></i> Speichern</button>
<button class="btn btn-outline-light" id="btn-import" title="Vorhaben aus JSON-Datei laden"><i class="bi bi-upload"></i> Laden</button>
<button class="btn btn-outline-light" id="btn-geojson" title="Flächen als GeoJSON (EPSG:25833) exportieren"><i class="bi bi-geo"></i> GeoJSON</button>
<button class="btn btn-outline-light" id="btn-print" title="Formblätter drucken"><i class="bi bi-printer"></i> Drucken</button>
<button class="btn btn-outline-light" id="btn-hilfe" title="Methodik und Bedienung"><i class="bi bi-question-circle"></i></button>
</div>
<input type="file" id="file-import" accept=".json,application/json" class="d-none">
</div>
</nav>
<div class="app-body">
<!-- Karte -->
<div class="map-col">
<div class="map-toolbar d-flex flex-wrap align-items-center gap-2 px-2 py-1">
<select class="form-select form-select-sm w-auto" id="sel-basis" title="Hintergrundkarte">
<option value="osm">OpenStreetMap</option>
<option value="dop">Luftbild (DOP GeoSN)</option>
<option value="webatlas">WebAtlasSN</option>
<option value="none">kein Hintergrund</option>
</select>
<div class="form-check form-check-inline form-switch m-0 small">
<input class="form-check-input" type="checkbox" id="chk-flst">
<label class="form-check-label" for="chk-flst">Flurstücke (ALKIS)</label>
</div>
<div class="input-group input-group-sm w-auto">
<input type="text" class="form-control" id="inp-suche" placeholder="Ort / Adresse suchen …" style="width:14rem">
<button class="btn btn-outline-secondary" id="btn-suche" title="Suchen (OSM Nominatim)"><i class="bi bi-search"></i></button>
</div>
<div class="btn-group btn-group-sm ms-auto" role="group" id="map-tools">
<button class="btn btn-outline-secondary active" data-tool="select" title="Auswählen / Verschieben der Karte"><i class="bi bi-cursor"></i></button>
<button class="btn btn-outline-secondary" data-tool="modify" title="Stützpunkte der gewählten Fläche bearbeiten"><i class="bi bi-pencil"></i></button>
<button class="btn btn-outline-secondary" data-tool="delete" title="Geometrie der gewählten Fläche löschen"><i class="bi bi-trash"></i></button>
<button class="btn btn-outline-danger" id="btn-draw-cancel" title="Zeichnen abbrechen (Esc)" hidden><i class="bi bi-x-lg"></i> Abbrechen</button>
</div>
</div>
<div id="map"></div>
<div class="map-status small px-2 py-1 d-flex gap-3">
<span id="map-hint" class="text-muted">Flächen über „Fläche zeichnen“ in den Tabs rechts anlegen. Klick auf eine Fläche wählt den zugehörigen Eintrag.</span>
<span class="ms-auto text-nowrap" id="map-coord"></span>
</div>
<div class="map-legend small">
<span><i class="lg lg-eingriff"></i>Eingriff</span>
<span><i class="lg lg-komp"></i>Kompensation</span>
<span><i class="lg lg-funk"></i>Funktionsraum</span>
<span><i class="lg lg-land"></i>Landschaftsbild</span>
<span><i class="lg lg-baum"></i>Einzelbaum</span>
</div>
</div>
<!-- Panel -->
<div class="panel-col">
<ul class="nav nav-tabs nav-fill small" id="tabs" role="tablist">
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-vorhaben">Vorhaben</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-eingriff">Eingriff <span class="badge rounded-pill text-bg-secondary" id="badge-eingriff">F1</span></button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-komp">Kompensation <span class="badge rounded-pill text-bg-secondary" id="badge-komp">0</span></button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-bilanz">Bilanz</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-form">Formblätter</button></li>
</ul>
<div class="tab-content panel-scroll">
<!-- Vorhaben -->
<div class="tab-pane fade show active p-3" id="tab-vorhaben">
<h6 class="text-uppercase text-muted small mb-2">Vorhaben</h6>
<div class="row g-2">
<div class="col-12"><label class="form-label small mb-0">Bezeichnung des Vorhabens</label><input class="form-control form-control-sm" data-meta="name" placeholder="z. B. Neubau Wirtschaftsweg W 12"></div>
<div class="col-6"><label class="form-label small mb-0">Verfahren / Verfahrenskennzahl</label><input class="form-control form-control-sm" data-meta="vkz"></div>
<div class="col-6"><label class="form-label small mb-0">Gemeinde / Gemarkung</label><input class="form-control form-control-sm" data-meta="ort"></div>
<div class="col-6"><label class="form-label small mb-0">Vorhabenträger</label><input class="form-control form-control-sm" data-meta="traeger"></div>
<div class="col-6"><label class="form-label small mb-0">Bearbeiter</label><input class="form-control form-control-sm" data-meta="bearbeiter"></div>
<div class="col-6"><label class="form-label small mb-0">Datum</label><input type="date" class="form-control form-control-sm" data-meta="datum"></div>
<div class="col-6"><label class="form-label small mb-0">Zuständige Naturschutzbehörde</label><input class="form-control form-control-sm" data-meta="unb"></div>
<div class="col-12"><label class="form-label small mb-0">Beschreibung / Wirkfaktoren</label><textarea class="form-control form-control-sm" rows="3" data-meta="beschreibung" placeholder="Vorhabenbeschreibung, bau-/anlage-/betriebsbedingte Wirkfaktoren, Einwirkungsbereich …"></textarea></div>
</div>
<h6 class="text-uppercase text-muted small mt-4 mb-2">Anwendungsvoraussetzungen</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="chk-einfachfall" data-meta="einfachfall">
<label class="form-check-label" for="chk-einfachfall">Einfachfall nach Anlage 1 – kein gutachterlicher Darlegungsbedarf (vereinfachte verbale Gegenüberstellung genügt)</label>
</div>
<details class="small mt-1"><summary class="text-primary">Liste der Einfachfälle (Anlage 1)</summary><ul id="liste-einfachfaelle" class="mt-1"></ul></details>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="chk-fallb" data-meta="fall_b">
<label class="form-check-label" for="chk-fallb">Werte und Funktionen besonderer Bedeutung (Anlage 3, Stufen mittel bis sehr hoch) voraussichtlich betroffen → Erfassung nach Anlage 3/4 erforderlich</label>
</div>
<h6 class="text-uppercase text-muted small mt-4 mb-2">Vermeidung (Anlage 5)</h6>
<textarea class="form-control form-control-sm" rows="3" data-meta="vermeidung" placeholder="Durchgeführte Vermeidungs-/Minderungsmaßnahmen und Begründung verbleibender Beeinträchtigungen (§ 15 Abs. 1 BNatSchG)"></textarea>
<h6 class="text-uppercase text-muted small mt-4 mb-2">Kompensationskonzept</h6>
<textarea class="form-control form-control-sm" rows="3" data-meta="konzept" placeholder="Funktionaler, räumlicher und zeitlicher Zusammenhang zwischen Eingriff und Kompensation (Anlage 6 Nr. 3)"></textarea>
</div>
<!-- Eingriff -->
<div class="tab-pane fade p-3" id="tab-eingriff">
<div class="d-flex align-items-center mb-2">
<h6 class="text-uppercase text-muted small mb-0">1 · Biotoptypen – allgemeine Lebensraumfunktion</h6>
<div class="btn-group btn-group-sm ms-auto">
<button class="btn btn-primary" id="btn-ef-draw"><i class="bi bi-pentagon"></i> Fläche zeichnen</button>
<button class="btn btn-outline-primary" id="btn-ef-add" title="Zeile ohne Geometrie (Fläche manuell eingeben)"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
<div id="list-ef" class="vstack gap-2"></div>
<div class="text-muted small mt-1">Bestand = Biotopwert (BW), Zustand nach Eingriff = Planungswert (PW) des Zielbiotoptyps; Versiegelung → z. B. 11.06.110 „Einzelgebäude ohne Begrünung“ (0 WP) oder 11.04.xxx Verkehrsflächen.</div>
<div class="d-flex align-items-center mt-4 mb-2">
<h6 class="text-uppercase text-muted small mb-0">2 · Werte und Funktionen besonderer Bedeutung (Anlage 3 / 4)</h6>
<div class="btn-group btn-group-sm ms-auto">
<button class="btn btn-primary" id="btn-fk-draw" title="Funktionsraum als Fläche zeichnen"><i class="bi bi-pentagon"></i> Funktionsraum</button>
<button class="btn btn-outline-primary" id="btn-fk-add" title="Zeile ohne Geometrie"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
<div id="list-fk" class="vstack gap-2"></div>
<div class="text-muted small mt-1">Beeinträchtigung: FMF × Grundfläche des Funktionsraums (Tab. 4-1). Aufwertungen im Planungszustand (z. B. Gestaltungsmaßnahmen am Eingriffsort) als „Funktionsherstellung/-erhöhung“ (FH/FE) eintragen → P2.</div>
<div class="d-flex align-items-center mt-4 mb-2">
<h6 class="text-uppercase text-muted small mb-0">3 · Landschaftsbild – landschaftsästhetische Funktion (Anlage 6.2)</h6>
</div>
<div id="box-land" class="card card-body p-2 small"></div>
<div class="card mt-4 border-primary">
<div class="card-body p-2 small" id="box-f1-summe"></div>
</div>
</div>
<!-- Kompensation -->
<div class="tab-pane fade p-3" id="tab-komp">
<div class="d-flex align-items-center mb-2 gap-2">
<h6 class="text-uppercase text-muted small mb-0 me-auto">Kompensationsmaßnahmen</h6>
<select class="form-select form-select-sm w-auto" id="sel-mtyp" title="Maßnahmentyp / Formblatt"></select>
<div class="btn-group btn-group-sm">
<button class="btn btn-success" id="btn-m-draw"><i class="bi bi-pentagon"></i> Fläche zeichnen</button>
<button class="btn btn-outline-success" id="btn-m-add" title="Maßnahme ohne Geometrie"><i class="bi bi-plus-lg"></i></button>
</div>
</div>
<div id="list-m" class="vstack gap-2"></div>
<div class="text-muted small mt-2">Anforderungen (Anlage 6): Aufwertung des Zielbiotoptyps um mind. 3 WP/m² gegenüber dem Bestand (oder Aufwertung von Funktionen nach Anlage 3/4); Biotoptypen der Kategorie D haben keinen Planungswert. Bei Maßnahmenkombinationen gilt nur der höhere Bonusfaktor.</div>
</div>
<!-- Bilanz -->
<div class="tab-pane fade p-3" id="tab-bilanz">
<div id="box-bilanz"></div>
</div>
<!-- Formblätter -->
<div class="tab-pane fade p-3" id="tab-form">
<div class="d-flex mb-2"><span class="text-muted small">Formblätter nach Anlage 7 (F1 – F14). Druck über <i class="bi bi-printer"></i>.</span><button class="btn btn-sm btn-outline-secondary ms-auto" id="btn-form-refresh"><i class="bi bi-arrow-clockwise"></i> Aktualisieren</button></div>
<div id="box-form" class="formblaetter"></div>
</div>
</div>
</div>
</div>
<!-- Modal: Auf-/Abschläge -->
<div class="modal fade" id="modal-aas" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header py-2"><h6 class="modal-title">Auf- und Abschläge (Anlage 2, Spalte 12)</h6><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body small" id="aas-body"></div>
<div class="modal-footer py-1">
<span class="me-auto" id="aas-summe"></span>
<button class="btn btn-sm btn-outline-secondary" id="aas-reset">Zurücksetzen</button>
<button class="btn btn-sm btn-primary" id="aas-ok">Übernehmen</button>
</div>
</div>
</div>
</div>
<!-- Modal: Biotoptyp-Info -->
<div class="modal fade" id="modal-bt" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header py-2"><h6 class="modal-title" id="bt-title">Biotoptyp</h6><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body small" id="bt-body"></div>
</div>
</div>
</div>
<!-- Modal: Hilfe -->
<div class="modal fade" id="modal-hilfe" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header py-2"><h6 class="modal-title">Methodik und Bedienung</h6><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body small">
<h6>Grundprinzip (Biotopwertverfahren)</h6>
<p>Jeder Fläche wird über ihren Biotoptyp ein <b>Biotopwert</b> (BW, 0–30 Wertpunkte je m²) zugeordnet. <b>Wertpunkte (WP) = Fläche [m²] × Wert [WP/m²]</b>. Der Kompensationsbedarf ergibt sich aus dem Vergleich des Zustands vor dem Eingriff (BW) mit dem Zustand nach dem Eingriff (Planungswert PW des neuen Biotoptyps). Kompensationsmaßnahmen werden umgekehrt mit der Differenz Planungswert (Zielbiotoptyp) minus Biotopwert (Ausgangszustand) bewertet.</p>
<h6>Formblätter (Anlage 7)</h6>
<ul>
<li><b>F1</b> Kompensationsbedarf: B1 = (A1 + A2) − (P1 + P2) für den Naturhaushalt, B2 = A3 − P3 für das Landschaftsbild.</li>
<li><b>F2–F11</b> Kompensationsleistung je Maßnahmentyp (L1 Naturhaushalt, L2 Landschaftsbild) mit den jeweiligen Bonusfaktoren (Entsiegelung BFE, PIK BFP, Wiedervernetzung BFV, Gewässerrevitalisierung BFR, Wald/Naturschutz BFN).</li>
<li><b>F12</b> Ersatzzahlung für mastenartige Eingriffe ab 50 m (Prozentsätze der Baukosten).</li>
<li><b>F13/F14</b> Gesamtbilanz: B1 − L1 ≤ 0 und B2 − L2 ≤ 0 → Eingriff vollständig kompensiert.</li>
</ul>
<h6>Werte und Funktionen besonderer Bedeutung (Anlage 3/4)</h6>
<p>Bei Betroffenheit der Stufen mittel/hoch/sehr hoch wird zusätzlich ein Funktionsminderungsfaktor (FMF, Tab. 4-1) mit der Grundfläche des Funktionsraums multipliziert; Kompensationsmaßnahmen erhalten analog einen Funktionsaufwertungsfaktor (FAF).</p>
<h6>Bedienung</h6>
<ul>
<li>Flächen direkt in der Karte zeichnen (Doppelklick beendet die Fläche). Die Fläche wird geodätisch in EPSG:25833 berechnet und kann manuell überschrieben werden.</li>
<li>Biotoptypen über die Auswahlliste mit Suchfeld wählen (Code, Bezeichnung, Wertpunkte). <i class="bi bi-info-circle"></i> zeigt Definition, Schutzstatus, KAG.</li>
<li>Auf-/Abschläge (Spalte 12 der Biotopwertliste) je Fläche über den Button „AAS“; nur die für den Biotoptyp zugelassenen Nummern werden angeboten.</li>
<li>Alle Eingaben werden im Browser zwischengespeichert (localStorage) und können als JSON gesichert/geladen werden.</li>
</ul>
<p class="text-muted">Datenbasis: Biotopwertliste Sachsen (Anlage 2) und Anlagen 1, 3–7 der „Grundlagen für die Anlagen zur Sächsischen Kompensationsverordnung“ (TU Dresden / Froelich &amp; Sporbeck, Stand 01/2017), auf Grundlage der Handlungsempfehlung zur Bewertung und Bilanzierung von Eingriffen im Freistaat Sachsen (SMUL 2009). Automatisch extrahierte Werte – im Zweifel Quelle prüfen.</p>
</div>
</div>
</div>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toasts"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.14.0-beta3/js/bootstrap-select.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.14.0-beta3/js/i18n/defaults-de_DE.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.15.0/proj4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/openlayers/10.6.1/dist/ol.js"></script>
<script src="js/bilanz.js"></script>
<script src="js/karte.js"></script>
<script src="js/app.js"></script>
</body>
</html>
+851
View File
@@ -0,0 +1,851 @@
/* app.js – Oberfläche: Zustand, Karten-Anbindung, Formulare, Formblätter */
(function () {
'use strict';
const $ = window.jQuery;
const LS_KEY = 'eingriffsbilanz.projekt.v1';
let P = null, FK = null, BWL = null;
let OPT_BW = '', OPT_PW = '', FUNK_ALL = [];
let drawCtx = null, aasCtx = null, saveTimer = null, selectedFid = null;
/* ------------------------------------------------------------ Helfer */
const num = BIL.num;
const fmtWP = n => (Math.round((num(n) + Number.EPSILON) * 100) / 100).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const fmtM2 = n => Math.round(num(n)).toLocaleString('de-DE');
const fmtF = n => (n === null || n === undefined || n === '') ? '–' : (+n).toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 2 });
const fmtEur = n => Math.round(num(n)).toLocaleString('de-DE') + ' €';
const esc = s => $('<div>').text(s === null || s === undefined ? '' : s).html();
const today = () => new Date().toISOString().slice(0, 10);
const uid = () => P.seq++;
const byBtc = btc => BIL.byBtc[btc];
const btLabel = btc => { const t = byBtc(btc); return t ? t.btc + ' ' + t.name : (btc || '–'); };
const signed = d => (d > 0 ? '+' : '') + d;
function toast(msg, cls) {
const $t = $('<div class="toast align-items-center text-bg-' + (cls || 'dark') + ' border-0" role="alert"><div class="d-flex"><div class="toast-body small">' + esc(msg) + '</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div></div>');
$('#toasts').append($t);
bootstrap.Toast.getOrCreateInstance($t[0], { delay: 3500 }).show();
$t.on('hidden.bs.toast', () => $t.remove());
}
function showTab(id) { bootstrap.Tab.getOrCreateInstance(document.querySelector('[data-bs-target="#' + id + '"]')).show(); }
function neuesProjekt() {
return {
version: 1, seq: 1,
meta: { name: '', vkz: '', ort: '', traeger: '', bearbeiter: '', datum: today(), unb: '', beschreibung: '', einfachfall: false, fall_b: false, vermeidung: '', konzept: '' },
eingriff: {
flaechen: [], funktionen: [],
landschaft: { typ: 'keiner', hoehe: '', radius1: '', radius2_override: '', standort_fid: null, anteile: { 1: {}, 2: {} }, fwz1: '', fwz2: '', fwz1_manuell: false, fwz2_manuell: false, aufwertungen: [], baukosten: '', abl: {}, repowering: 'keins' }
},
massnahmen: [],
features: { type: 'FeatureCollection', features: [] }
};
}
/* ----------------------------------------------------- Persistenz */
function scheduleSave() { clearTimeout(saveTimer); saveTimer = setTimeout(save, 400); }
function snapshot() { P.features = KARTE.toGeoJSON(); return P; }
function save() {
try {
localStorage.setItem(LS_KEY, JSON.stringify(snapshot()));
$('#autosave-info').text('zwischengespeichert ' + new Date().toLocaleTimeString('de-DE'));
} catch (e) { console.warn(e); }
}
function loadProjekt(obj) {
const fresh = neuesProjekt();
P = Object.assign(fresh, obj || {});
P.meta = Object.assign(fresh.meta, P.meta || {});
P.eingriff = Object.assign(fresh.eingriff, P.eingriff || {});
P.eingriff.landschaft = Object.assign(fresh.eingriff.landschaft, P.eingriff.landschaft || {});
P.eingriff.landschaft.anteile = Object.assign({ 1: {}, 2: {} }, P.eingriff.landschaft.anteile || {});
P.massnahmen = P.massnahmen || [];
KARTE.loadGeoJSON(P.features);
renderAll();
KARTE.fitAll();
updateZonen();
}
function download(name, text, mime) {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([text], { type: mime || 'application/json' }));
a.download = name; document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 500);
}
const dateiname = suffix => ((P.meta.name || 'vorhaben').replace(/[^\wäöüÄÖÜß-]+/g, '_') + suffix);
/* --------------------------------------------- Optionen Biotoptypen */
function buildOptions() {
const grp = {};
BWL.hauptgruppen.forEach(h => { grp[h.nr] = { name: h.name, bw: [], pw: [] }; });
BWL.typen.forEach(t => {
if (t.ebene !== 'blatt') return;
const sub = 'BW ' + (t.bw ?? '–') + ' · PW ' + (t.pw ?? '–') + ' · KAG ' + (t.kag || '–') + (t.schutz !== '–' ? ' · ' + t.schutz : '') + (t.lrt !== '–' ? ' · LRT ' + t.lrt : '');
const opt = '<option value="' + t.btc + '" data-subtext="' + esc(sub) + '" data-tokens="' + esc(t.name + ' ' + t.btc.replace(/\./g, '')) + '">' + t.btc + ' ' + esc(t.name) + '</option>';
if (t.bw !== null) grp[t.hg].bw.push(opt);
if (t.pw !== null) grp[t.hg].pw.push(opt);
});
OPT_BW = ''; OPT_PW = '';
Object.keys(grp).forEach(k => {
const label = k + ' ' + esc(grp[k].name);
if (grp[k].bw.length) OPT_BW += '<optgroup label="' + label + '">' + grp[k].bw.join('') + '</optgroup>';
if (grp[k].pw.length) OPT_PW += '<optgroup label="' + label + '">' + grp[k].pw.join('') + '</optgroup>';
});
}
function btSelect(field, value, art, title) {
const t = title || 'Biotoptyp wählen …';
return '<select class="bt-select" data-f="' + field + '" data-art="' + art + '" title="' + t + '"><option value="">' + t + '</option>' + (art === 'bw' ? OPT_BW : OPT_PW) + '</select>';
}
function initSelects($root) {
$root.find('select.bt-select').each(function () {
const $s = $(this);
if ($s.data('selectpicker')) return;
const val = $s.attr('data-value');
$s.val(val || '');
$s.selectpicker({ liveSearch: true, size: 10, width: '100%', liveSearchPlaceholder: 'Suchen: Code oder Bezeichnung', noneSelectedText: $s.attr('title') || 'Biotoptyp wählen …', showSubtext: true, liveSearchNormalize: true, dropupAuto: false, container: 'body' });
});
}
function optList(list, value, keyF, labelF) {
return list.map(o => { const k = keyF(o); return '<option value="' + esc(k) + '"' + (String(k) === String(value) ? ' selected' : '') + '>' + esc(labelF(o)) + '</option>'; }).join('');
}
const stufenOpts = v => optList(FK.bewertungsstufen, v, s => s, s => s);
const ewbOpts = (v, list) => optList(list || ['UE', 'EE'], v, s => s, s => s + ' – ' + FK.ewb[s]);
const afbOpts = (v, keys) => optList(keys, v, s => s, s => s + ' – ' + FK.afb[s]);
const funkOpts = (v, list) => optList(list, v, f => f.key, f => f.name);
/* ---------------------------------------------------------- AAS */
function aasText(list) {
const s = BIL.aasSumme(list);
if (!list || !list.length) return '±0';
return signed(s) + ' (' + list.map(a => a.key || 'frei').join(', ') + ')';
}
function openAas(ctx) {
aasCtx = ctx;
const rec = ctx.rec, bt = byBtc(rec[ctx.btField]);
if (!bt) { toast('Zuerst Biotoptyp wählen.', 'warning'); return; }
const erlaubt = ctx.bestandSeite ? bt.aas_b : bt.aas_p;
const cur = rec[ctx.aasField] || [];
let h = '<p class="mb-2"><b>' + esc(bt.btc + ' ' + bt.name) + '</b> – ' + (ctx.bestandSeite ? 'Biotopwert ' + bt.bw : 'Planungswert ' + bt.pw) + ' WP/m². Zulässige Auf-/Abschläge lt. Spalte 12: ' + (erlaubt.length ? erlaubt.join(', ') : 'keine') + '.</p>';
FK.aas.filter(a => erlaubt.includes(a.nr)).forEach(a => {
h += '<div class="border rounded p-2 mb-2"><div class="fw-semibold mb-1">AAS ' + a.nr + ' – ' + esc(a.titel) + '</div>';
const gruppen = a.gruppen || [null];
gruppen.forEach(g => {
const name = 'aas-' + a.nr + (g ? '-' + g : '');
if (g) h += '<div class="text-muted mt-1">' + esc(g) + '</div>';
const opts = a.optionen.filter(o => !g || o.gruppe === g);
const selKey = (cur.find(c => opts.some(o => o.key === c.key)) || {}).key || '';
h += '<div class="form-check"><input class="form-check-input" type="radio" name="' + name + '" value="" id="' + name + '-0"' + (selKey ? '' : ' checked') + '><label class="form-check-label" for="' + name + '-0">kein Ansatz</label></div>';
opts.forEach(o => {
h += '<div class="form-check"><input class="form-check-input" type="radio" name="' + name + '" value="' + o.key + '" data-delta="' + o.delta + '" id="' + name + '-' + o.key + '"' + (selKey === o.key ? ' checked' : '') + '><label class="form-check-label" for="' + name + '-' + o.key + '"><span class="badge ' + (o.delta < 0 ? 'text-bg-danger' : 'text-bg-success') + ' me-1">' + signed(o.delta) + '</span>' + esc(o.text) + '</label></div>';
});
});
h += '</div>';
});
const frei = cur.find(c => c.key === 'frei') || {};
h += '<div class="border rounded p-2 mb-2"><div class="fw-semibold mb-1">Abweichung in Absprache mit der Naturschutzbehörde</div><div class="row g-2 align-items-center"><div class="col-auto"><input type="number" class="form-control form-control-sm" id="aas-frei" min="-5" max="5" step="1" style="width:5rem" value="' + (frei.delta ?? '') + '"></div><div class="col"><input type="text" class="form-control form-control-sm" id="aas-frei-txt" placeholder="Begründung" value="' + esc(frei.text || '') + '"></div></div></div>';
$('#aas-body').html(h);
updateAasSumme();
bootstrap.Modal.getOrCreateInstance('#modal-aas').show();
}
function readAas() {
const list = [];
$('#aas-body input[type=radio]:checked').each(function () {
if (!this.value) return;
const nr = parseInt(this.name.split('-')[1], 10);
const a = FK.aas.find(x => x.nr === nr); const o = a.optionen.find(x => x.key === this.value);
list.push({ key: o.key, nr, delta: o.delta, text: o.text });
});
const frei = num($('#aas-frei').val());
if (frei) list.push({ key: 'frei', nr: 0, delta: BIL.clamp(frei, -5, 5), text: $('#aas-frei-txt').val() });
return list;
}
function updateAasSumme() {
const list = readAas(); const s = BIL.aasSumme(list);
const bt = byBtc(aasCtx.rec[aasCtx.btField]); const base = aasCtx.bestandSeite ? bt.bw : bt.pw;
const eff = BIL.clamp(base + s, FK.wp_min, FK.wp_max);
$('#aas-summe').html('Summe <b>' + signed(s) + '</b> → Wert <b>' + eff + ' WP/m²</b>' + (Math.abs(s) > FK.aas_max_abs ? ' <span class="warn">(Einzelabschlag max. ±5, Wert 0–30)</span>' : ''));
}
$('#aas-body').on('change', 'input', updateAasSumme);
$('#aas-reset').on('click', () => { $('#aas-body input[type=radio][value=""]').prop('checked', true); $('#aas-frei').val(''); $('#aas-frei-txt').val(''); updateAasSumme(); });
$('#aas-ok').on('click', () => {
aasCtx.rec[aasCtx.aasField] = readAas();
bootstrap.Modal.getOrCreateInstance('#modal-aas').hide();
aasCtx.$card.find('[data-aas="' + aasCtx.aasField + '"]').text(aasText(aasCtx.rec[aasCtx.aasField]));
refreshCalc(); scheduleSave();
});
/* ------------------------------------------------- Biotoptyp-Info */
function showBtInfo(btc) {
const t = byBtc(btc); if (!t) return;
const kag = (t.kag || '').split('/').map(k => '<li><b>' + k + '</b> – ' + esc(FK.kag[k] || '') + '</li>').join('');
const aasB = FK.aas.filter(a => t.aas_b.includes(a.nr)).map(a => a.nr + ' ' + a.titel).join('; ') || 'keine';
const aasP = FK.aas.filter(a => t.aas_p.includes(a.nr)).map(a => a.nr + ' ' + a.titel).join('; ') || 'keine';
$('#bt-title').text(t.btc + ' ' + t.name);
$('#bt-body').html(
'<table class="table table-sm mb-2"><tr><th>Biotopwert (Bestand)</th><td>' + (t.bw ?? '–') + ' WP/m² ' + (t.bw_alt !== null && t.bw_alt !== undefined ? '(alt. ' + t.bw_alt + ')' : '') + ' – ' + esc(BIL.stufe(t.bw)) + '</td></tr>' +
'<tr><th>Planungswert (Ziel)</th><td>' + (t.pw ?? '–') + ' WP/m² ' + (t.pw_alt !== null && t.pw_alt !== undefined ? '(alt. ' + t.pw_alt + ')' : '') + '</td></tr>' +
'<tr><th>Kategorie Ausgleichbarkeit</th><td>' + (t.kag || '–') + '<ul class="mb-0">' + kag + '</ul></td></tr>' +
'<tr><th>Gesetzlicher Schutz</th><td>' + esc(t.schutz) + ' <span class="text-muted">(§ grundsätzlich, (§) bestimmte Ausprägungen, ((§)) bei Lage im Überschwemmungsbereich)</span></td></tr>' +
'<tr><th>FFH-LRT</th><td>' + esc(t.lrt) + '</td></tr><tr><th>Rote Liste Sachsen</th><td>' + esc(t.rl) + '</td></tr>' +
'<tr><th>AAS Bestand</th><td>' + esc(aasB) + '</td></tr><tr><th>AAS Planung</th><td>' + esc(aasP) + '</td></tr>' +
(t.btlnk ? '<tr><th>BTLNK / SBK</th><td class="text-muted">' + esc(t.btlnk) + ' / ' + esc(t.sbk) + '</td></tr>' : '') + '</table>' +
(t.definition ? '<p><b>Definition (Anlage 2):</b> ' + esc(t.definition) + '</p>' : '<p class="text-muted">Definition siehe Biotoptypenliste Sachsen (LfUG 2004 / Buder &amp; Uhlemann 2010).</p>')
);
bootstrap.Modal.getOrCreateInstance('#modal-bt').show();
}
/* ----------------------------------------------------- Meta-Daten */
function renderMeta() {
$('[data-meta]').each(function () {
const k = $(this).data('meta');
if (this.type === 'checkbox') this.checked = !!P.meta[k]; else $(this).val(P.meta[k] ?? '');
});
$('#liste-einfachfaelle').html(FK.einfachfaelle.map(e => '<li>' + esc(e) + '</li>').join(''));
}
$(document).on('change input', '[data-meta]', function () {
const k = $(this).data('meta');
P.meta[k] = this.type === 'checkbox' ? this.checked : this.value;
scheduleSave();
if (k === 'einfachfall' || k === 'fall_b') refreshCalc();
});
/* ----------------------------------------------- Eingriffsflächen */
function findEF(id) { return P.eingriff.flaechen.find(f => f.id == id); }
function efCard(f) {
const istBaum = !!f.kronen_dm || (KARTE.getFeature(f.fid) && KARTE.getFeature(f.fid).get('rolle') === 'baum');
return '<div class="card card-ef" data-kind="ef" data-id="' + f.id + '">' +
'<div class="card-body">' +
'<div class="d-flex align-items-center gap-2 mb-1"><span class="badge text-bg-danger">E' + f.id + '</span>' +
'<input class="form-control form-control-sm" data-f="bez" value="' + esc(f.bez) + '" placeholder="Bezeichnung der Fläche">' +
'<div class="btn-group btn-group-sm ms-auto flex-shrink-0">' +
'<button class="btn btn-outline-secondary btn-zoom" title="In Karte zeigen"' + (f.fid ? '' : ' disabled') + '><i class="bi bi-crosshair"></i></button>' +
'<button class="btn btn-outline-secondary btn-redraw" title="Fläche (neu) zeichnen"><i class="bi bi-pentagon"></i></button>' +
'<button class="btn btn-outline-danger btn-del" title="Fläche löschen"><i class="bi bi-trash"></i></button></div></div>' +
'<div class="row g-2">' +
'<div class="col-6"><label class="form-label">Bestand (vor Eingriff)</label>' + btSelect('bt_bestand', f.bt_bestand, 'bw').replace('<select ', '<select data-value="' + esc(f.bt_bestand || '') + '" ') +
'<div class="d-flex align-items-center gap-1 mt-1"><button class="btn btn-xs btn-outline-secondary btn-aas" data-bt="bt_bestand" data-aas-field="aas_bestand" data-bestand="1">AAS <span data-aas="aas_bestand">' + aasText(f.aas_bestand) + '</span></button>' +
'<button class="btn btn-xs btn-outline-secondary btn-btinfo" data-bt="bt_bestand" title="Details zum Biotoptyp"><i class="bi bi-info-circle"></i></button>' +
'<span class="ms-auto small">BW <b class="badge text-bg-light wert-badge" data-w="bw">–</b></span></div></div>' +
'<div class="col-6"><label class="form-label">Zustand nach Eingriff</label>' + btSelect('bt_planung', f.bt_planung, 'pw').replace('<select ', '<select data-value="' + esc(f.bt_planung || '') + '" ') +
'<div class="d-flex align-items-center gap-1 mt-1"><button class="btn btn-xs btn-outline-secondary btn-aas" data-bt="bt_planung" data-aas-field="aas_planung">AAS <span data-aas="aas_planung">' + aasText(f.aas_planung) + '</span></button>' +
'<button class="btn btn-xs btn-outline-secondary btn-btinfo" data-bt="bt_planung" title="Details zum Biotoptyp"><i class="bi bi-info-circle"></i></button>' +
'<span class="ms-auto small">PW <b class="badge text-bg-light wert-badge" data-w="pw">–</b></span></div></div></div>' +
'<div class="d-flex align-items-center gap-2 mt-2 small flex-wrap">' +
(istBaum ? '<label class="form-label mb-0">Kronen-Ø</label><div class="input-group input-group-sm" style="width:7rem"><input type="number" class="form-control" data-f="kronen_dm" min="0" step="0.5" value="' + esc(f.kronen_dm || '') + '"><span class="input-group-text">m</span></div>' : '') +
'<label class="form-label mb-0">Fläche</label><div class="input-group input-group-sm" style="width:12rem"><input type="number" class="form-control" data-f="gbf" min="0" step="1" value="' + esc(f.gbf ?? '') + '"><span class="input-group-text">m²</span>' +
'<button class="btn btn-outline-secondary btn-gbf-map" title="Fläche aus Karte übernehmen"' + (f.fid ? '' : ' disabled') + '><i class="bi bi-arrow-repeat"></i></button></div>' +
'<span class="text-muted" data-w="gbfinfo">' + (f.fid ? (f.gbf_manuell ? 'manuell' : 'aus Karte') : 'ohne Geometrie') + '</span>' +
'<span class="ms-auto">WP Bestand <b data-w="wpb">0</b> · nach Eingriff <b data-w="wpp">0</b> · Wertminderung <b data-w="diff">0</b></span></div>' +
'</div></div>';
}
function renderEF() {
const $l = $('#list-ef').empty();
if (!P.eingriff.flaechen.length) $l.html('<div class="text-muted small border rounded p-2">Noch keine Eingriffsflächen. Fläche in der Karte zeichnen oder Zeile anlegen.</div>');
P.eingriff.flaechen.forEach(f => $l.append(efCard(f)));
initSelects($l);
}
function addEF(fid, rolle) {
const f = { id: uid(), fid: fid || null, bez: '', bt_bestand: '', aas_bestand: [], bt_planung: '', aas_planung: [], gbf: '', gbf_manuell: false, kronen_dm: rolle === 'baum' ? 8 : '' };
P.eingriff.flaechen.push(f);
if (fid) { KARTE.setProps(fid, { label: 'E' + f.id, ref: 'ef:' + f.id, kronen_dm: f.kronen_dm || undefined }); syncArea(f); }
renderEF(); refreshCalc(); scheduleSave();
return f;
}
/* ------------------------------------------- Funktionen (Eingriff) */
function findFK(id) { return P.eingriff.funktionen.find(f => f.id == id); }
function fkCard(k) {
return '<div class="card card-fk" data-kind="fk" data-id="' + k.id + '"><div class="card-body">' +
'<div class="d-flex align-items-center gap-2 mb-1"><span class="badge text-bg-warning text-dark">F' + k.id + '</span>' +
'<select class="form-select form-select-sm" data-f="funktion">' + funkOpts(k.funktion, FK.funktionen) + '</select>' +
'<div class="btn-group btn-group-sm ms-auto flex-shrink-0">' +
'<button class="btn btn-outline-secondary btn-zoom" title="In Karte zeigen"' + (k.fid ? '' : ' disabled') + '><i class="bi bi-crosshair"></i></button>' +
'<button class="btn btn-outline-secondary btn-redraw" title="Funktionsraum (neu) zeichnen"><i class="bi bi-pentagon"></i></button>' +
'<button class="btn btn-outline-danger btn-del"><i class="bi bi-trash"></i></button></div></div>' +
'<div class="text-muted small mb-1" data-w="funkkurz"></div>' +
'<div class="row g-2 align-items-end">' +
'<div class="col-3"><label class="form-label">Einwirkungsbereich</label><select class="form-select form-select-sm" data-f="ewb">' + ewbOpts(k.ewb) + '</select></div>' +
'<div class="col-3"><label class="form-label">Bewertungsstufe</label><select class="form-select form-select-sm" data-f="bsf">' + stufenOpts(k.bsf) + '</select></div>' +
'<div class="col-3"><label class="form-label">Betroffenheit</label><select class="form-select form-select-sm" data-f="afb">' + afbOpts(k.afb, ['FV', 'FS', 'FH', 'FE']) + '</select></div>' +
'<div class="col-3"><label class="form-label">Faktor (Tab. 4-1)</label><div class="input-group input-group-sm"><span class="input-group-text" data-w="faktor">–</span><input type="number" class="form-control" data-f="faktor_override" step="0.1" min="0" max="2" placeholder="abw." value="' + esc(k.faktor_override ?? '') + '" title="Abweichender Faktor (Begründung erforderlich)"></div></div>' +
'</div>' +
'<div class="d-flex align-items-center gap-2 mt-2 small flex-wrap"><label class="form-label mb-0">Grundfläche Funktionsraum</label>' +
'<div class="input-group input-group-sm" style="width:12rem"><input type="number" class="form-control" data-f="gbf" min="0" step="1" value="' + esc(k.gbf ?? '') + '"><span class="input-group-text">m²</span><button class="btn btn-outline-secondary btn-gbf-map" title="Fläche aus Karte übernehmen"' + (k.fid ? '' : ' disabled') + '><i class="bi bi-arrow-repeat"></i></button></div>' +
'<input class="form-control form-control-sm" data-f="bemerkung" style="max-width:16rem" placeholder="Begründung / Datenquelle" value="' + esc(k.bemerkung || '') + '">' +
'<span class="ms-auto"><span data-w="seite"></span> <b data-w="wp">0</b> WP</span></div>' +
'</div></div>';
}
function renderFK() {
const $l = $('#list-fk').empty();
if (!P.eingriff.funktionen.length) $l.html('<div class="text-muted small border rounded p-2">Nur bei Betroffenheit von Werten und Funktionen besonderer Bedeutung (Stufen mittel bis sehr hoch).</div>');
P.eingriff.funktionen.forEach(k => $l.append(fkCard(k)));
}
function addFK(fid) {
const k = { id: uid(), fid: fid || null, funktion: FK.funktionen[0].key, ewb: 'UE', bsf: 'hoch', afb: 'FV', gbf: '', gbf_manuell: false, faktor_override: '', bemerkung: '' };
P.eingriff.funktionen.push(k);
if (fid) { KARTE.setProps(fid, { label: 'F' + k.id, ref: 'fk:' + k.id }); syncArea(k); }
renderFK(); refreshCalc(); scheduleSave();
return k;
}
/* ------------------------------------------------- Landschaftsbild */
function aufwRows(list, kind) {
return list.map(a => '<div class="faf-row row g-1 align-items-end" data-kind="' + kind + '" data-id="' + a.id + '">' +
'<div class="col-3"><input class="form-control form-control-sm" data-f="bez" placeholder="Maßnahme / Element" value="' + esc(a.bez || '') + '"></div>' +
'<div class="col-2"><select class="form-select form-select-sm" data-f="bsf">' + stufenOpts(a.bsf) + '</select></div>' +
'<div class="col-2"><select class="form-select form-select-sm" data-f="afb">' + afbOpts(a.afb, ['FH', 'FE']) + '</select></div>' +
'<div class="col-2"><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="gbf" placeholder="m²" value="' + esc(a.gbf ?? '') + '"></div></div>' +
'<div class="col-2"><div class="input-group input-group-sm"><span class="input-group-text" data-w="faktor">–</span><input type="number" class="form-control" data-f="faktor_override" step="0.1" placeholder="abw." value="' + esc(a.faktor_override ?? '') + '"></div></div>' +
'<div class="col-1 text-end"><b data-w="wp">0</b><button class="btn btn-xs btn-outline-danger btn-del ms-1"><i class="bi bi-x"></i></button></div></div>').join('');
}
function renderLand() {
const L = P.eingriff.landschaft; const LB = FK.landschaftsbild;
const typ = LB.eingriffstypen.find(t => t.key === L.typ) || LB.eingriffstypen[0];
let h = '<div class="row g-2 align-items-end"><div class="col-12"><label class="form-label">Art des Eingriffs in das Landschaftsbild</label><select class="form-select form-select-sm" data-land="typ" data-rerender="1">' + optList(LB.eingriffstypen, L.typ, t => t.key, t => t.name) + '</select></div>';
if (typ.key !== 'keiner') {
const mast = typ.key.startsWith('mast');
h += '<div class="col-3"><label class="form-label">Gesamthöhe</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-land="hoehe" min="0" step="0.5" value="' + esc(L.hoehe) + '"><span class="input-group-text">m</span></div></div>';
h += '<div class="col-3"><label class="form-label">Radius Nahzone (UE)</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-land="radius1" min="' + (typ.radius1_min || 200) + '" step="10" placeholder="' + (typ.radius1 || 200) + '" value="' + esc(L.radius1) + '"' + (mast ? ' readonly' : '') + '><span class="input-group-text">m</span></div></div>';
if (typ.zone2) h += '<div class="col-3"><label class="form-label">Radius EE (25 × Höhe)</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-land="radius2_override" min="0" step="10" placeholder="' + Math.round(BIL.clamp(LB.radius_faktor_hoehe * num(L.hoehe), 0, LB.radius_max)) + '" value="' + esc(L.radius2_override) + '"><span class="input-group-text">m</span></div></div>';
h += '<div class="col-3"><label class="form-label">Standort in der Karte</label><div class="btn-group btn-group-sm w-100"><button class="btn btn-outline-primary" id="btn-land-standort"><i class="bi bi-geo-alt"></i> ' + (L.standort_fid ? 'neu setzen' : 'setzen') + '</button><button class="btn btn-outline-secondary" id="btn-land-zoom"' + (L.standort_fid ? '' : ' disabled') + '><i class="bi bi-crosshair"></i></button></div></div>';
h += '</div>';
if (typ.key === 'mast_ab50') {
h += '<div class="mt-3 fw-semibold">Ersatzzahlung (F12) – Prozentsätze der Baukosten nach Bewertungsstufe des Landschaftsbildes</div>' +
'<div class="row g-2 align-items-end mt-1"><div class="col-4"><label class="form-label">Baukosten gesamt (DIN 276)</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-land="baukosten" min="0" step="1000" value="' + esc(L.baukosten) + '"><span class="input-group-text">€</span></div></div>' +
'<div class="col-8"><label class="form-label">Repowering</label><select class="form-select form-select-sm" data-land="repowering">' + optList(LB.repowering, L.repowering, r => r.key, r => r.name) + '</select></div></div>' +
'<table class="table table-sm mt-2 mb-1"><thead><tr><th>Bewertungsstufe</th><th class="text-end">Anteil am Einwirkungsbereich %</th><th class="text-end">Kostensatz</th><th class="text-end">Ersatzzahlung</th></tr></thead><tbody>';
LB.stufen_ersatzzahlung.forEach(s => { h += '<tr><td>' + s + '</td><td><input type="number" class="form-control form-control-sm text-end" data-land="abl.' + s + '" min="0" max="100" step="1" value="' + esc((L.abl || {})[s] ?? '') + '"></td><td class="text-end">' + LB.ersatzzahlung_prozent[s] + ' %</td><td class="text-end" data-w="esz-' + s.replace(' ', '_') + '">–</td></tr>'; });
h += '</tbody></table><div class="d-flex"><span data-w="ablsum" class="text-muted"></span><span class="ms-auto">Ersatzzahlung gesamt (E1): <b class="wp-big" data-w="E1">0 €</b></span></div>';
} else {
h += '<div class="mt-3 fw-semibold">Wirkzonen – Anteile der Bewertungsstufen (Anlage 3) an der Zonenfläche</div><div class="text-muted">Wertpunkte je m²: ' + fmtF(typ.wpk1) + ' (Zone 1)' + (typ.zone2 ? ', ' + fmtF(typ.wpk2) + ' (Zone 2)' : '') + '; Funktionssenkungsfaktor sehr hoch 1,5 · hoch 1,0 · mittel 0,5. Stufen gering/sehr gering lösen keinen Bedarf aus.</div>' +
'<table class="table table-sm mt-1 mb-1"><thead><tr><th>Zone</th><th class="text-end">Zonenfläche m²</th><th class="text-end">sehr hoch %</th><th class="text-end">hoch %</th><th class="text-end">mittel %</th><th class="text-end">WP</th></tr></thead><tbody>';
[1, 2].filter(z => z === 1 || typ.zone2).forEach(z => {
const man = L['fwz' + z + '_manuell'];
h += '<tr><td data-w="zname' + z + '">Zone ' + z + '</td><td><div class="input-group input-group-sm"><input type="number" class="form-control text-end" data-land="fwz' + z + '" value="' + esc(L['fwz' + z]) + '"' + (man ? '' : ' readonly') + '><button class="btn btn-outline-secondary btn-fwz-toggle" data-z="' + z + '" title="' + (man ? 'automatisch aus Radius' : 'manuell eingeben') + '"><i class="bi ' + (man ? 'bi-unlock' : 'bi-lock') + '"></i></button></div></td>' +
['sehr hoch', 'hoch', 'mittel'].map(s => '<td><input type="number" class="form-control form-control-sm text-end" data-land="anteile.' + z + '.' + s + '" min="0" max="100" step="1" value="' + esc(((L.anteile || {})[z] || {})[s] ?? '') + '"></td>').join('') +
'<td class="text-end" data-w="zwp' + z + '">0</td></tr>';
});
h += '</tbody></table><div class="text-end">Kompensationsbedarf Landschaftsbild A3: <b data-w="A3">0</b> WP</div>';
}
}
h += '<div class="mt-3 d-flex align-items-center"><span class="fw-semibold">Aufwertung der landschaftsästhetischen Funktion im Planungszustand (P3)</span><button class="btn btn-xs btn-outline-primary ms-auto" id="btn-land-aufw"><i class="bi bi-plus-lg"></i> Zeile</button></div>' +
'<div id="list-land-aufw">' + aufwRows(L.aufwertungen, 'la') + '</div>';
$('#box-land').html(h);
}
$(document).on('change', '[data-land]', function () {
const path = $(this).data('land').split('.');
let o = P.eingriff.landschaft;
for (let i = 0; i < path.length - 1; i++) { o[path[i]] = o[path[i]] || {}; o = o[path[i]]; }
o[path[path.length - 1]] = this.type === 'checkbox' ? this.checked : this.value;
if ($(this).data('rerender')) renderLand();
updateZonen(); refreshCalc(); scheduleSave();
});
$(document).on('click', '.btn-fwz-toggle', function () {
const z = $(this).data('z'); const L = P.eingriff.landschaft;
L['fwz' + z + '_manuell'] = !L['fwz' + z + '_manuell'];
if (!L['fwz' + z + '_manuell']) L['fwz' + z] = '';
renderLand(); refreshCalc(); scheduleSave();
});
$(document).on('click', '#btn-land-standort', () => { startDraw({ kind: 'standort' }, 'standort', 'Point'); });
$(document).on('click', '#btn-land-zoom', () => KARTE.zoomTo(P.eingriff.landschaft.standort_fid));
$(document).on('click', '#btn-land-aufw', () => { P.eingriff.landschaft.aufwertungen.push({ id: uid(), bez: '', bsf: 'mittel', afb: 'FH', gbf: '', faktor_override: '' }); renderLand(); refreshCalc(); scheduleSave(); });
function updateZonen() {
const L = P.eingriff.landschaft;
if (!L.standort_fid || L.typ === 'keiner') { KARTE.clearZonen(); return; }
const land = BIL.calcLandschaft(L);
KARTE.setZonen(L.standort_fid, land.radius1 || 0, land.radius2 || 0);
}
/* --------------------------------------------------- Maßnahmen */
function findM(id) { return P.massnahmen.find(m => m.id == id); }
function typInfo(key) { return FK.massnahmentypen.find(t => t.key === key) || FK.massnahmentypen[0]; }
function paramsHtml(m) {
const p = m.params || {};
switch (m.typ) {
case 'F2':
return '<div class="form-check form-check-inline small"><input class="form-check-input" type="checkbox" data-f="params.baum" data-rerender="1" id="baum-' + m.id + '"' + (p.baum ? ' checked' : '') + '><label class="form-check-label" for="baum-' + m.id + '">Baumpflanzung nach Anlage 6.1 (pauschale Kronentrauffläche)</label></div>' +
(p.baum ? '<div class="row g-2 mt-1"><div class="col-4"><label class="form-label">Anzahl Bäume</label><input type="number" class="form-control form-control-sm" data-f="params.anzahl" min="0" step="1" value="' + esc(p.anzahl ?? '') + '"></div><div class="col-8"><label class="form-label">Baumart</label><select class="form-select form-select-sm" data-f="params.baumart"><option value="laub"' + (p.baumart !== 'obst' ? ' selected' : '') + '>Laubbaum Hochstamm – 30 m² je Baum</option><option value="obst"' + (p.baumart === 'obst' ? ' selected' : '') + '>Obstbaum Hochstamm – 20 m² je Baum</option></select></div></div>' : '');
case 'F3':
return '<label class="form-label">Kategorie der Bauwerkshöhe → Bonusfaktor Entsiegelung</label><select class="form-select form-select-sm" data-f="params.kbh">' + optList(FK.entsiegelung.kbh, p.kbh ?? 0, k => k.kbh, k => k.name + ' – BFE ' + fmtF(k.bfe)) + '</select><div class="text-muted small mt-1">' + esc(FK.entsiegelung.hinweis) + '</div>';
case 'F4': {
const pik = FK.pik.massnahmen.find(x => x.key === p.pik) || FK.pik.massnahmen[0];
return '<label class="form-label">PIK-Maßnahme (Steckbrief Anlage 6.4)</label><select class="form-select form-select-sm" data-f="params.pik" data-rerender="1">' + optList(FK.pik.massnahmen.filter(x => x.formblatt === 'F4'), pik.key, x => x.key, x => x.nr + ' – ' + x.name + ' (BFP ' + fmtF(x.bfp) + ')') + '</select>' +
'<div class="d-flex gap-3 mt-1 small">' + (pik.zusatz ? '<div class="form-check"><input class="form-check-input" type="checkbox" data-f="params.zfk" id="zfk-' + m.id + '"' + (p.zfk ? ' checked' : '') + '><label class="form-check-label" for="zfk-' + m.id + '">Kombination ≥ 2 PIK (+0,2)</label></div><div class="form-check"><input class="form-check-input" type="checkbox" data-f="params.zfz" id="zfz-' + m.id + '"' + (p.zfz ? ' checked' : '') + '><label class="form-check-label" for="zfz-' + m.id + '">Zusatzanforderung erfüllt (+0,2)</label></div>' : '<span class="text-muted">keine Bonuserhöhung möglich</span>') +
(pik.bfp_max ? '<div class="input-group input-group-sm ms-auto" style="width:9rem"><span class="input-group-text">BFP</span><input type="number" class="form-control" data-f="params.bfp_override" step="0.1" min="' + pik.bfp + '" max="' + pik.bfp_max + '" placeholder="' + pik.bfp + '" value="' + esc(p.bfp_override ?? '') + '"></div>' : '') + '</div>' +
(pik.ausgang.length ? '<div class="text-muted small mt-1">Ausgang: ' + pik.ausgang.join(', ') + (pik.ziel.length ? ' · Ziel: ' + pik.ziel.join(', ') : ' · Ziel: nicht degenerierter Pendant-Biotoptyp') + '</div>' : '');
}
case 'F5': {
const pik = FK.pik.massnahmen.find(x => x.key === p.pik) || FK.pik.massnahmen.find(x => x.formblatt === 'F5');
return '<label class="form-label">Artenschutz-PIK</label><select class="form-select form-select-sm" data-f="params.pik" data-rerender="1">' + optList(FK.pik.massnahmen.filter(x => x.formblatt === 'F5'), pik.key, x => x.key, x => x.name + ' (BFP ' + fmtF(x.bfp) + ')') + '</select>' +
'<div class="d-flex gap-3 mt-1 small align-items-center"><div class="form-check"><input class="form-check-input" type="checkbox" data-f="params.zfk" id="zfk-' + m.id + '"' + (p.zfk ? ' checked' : '') + '><label class="form-check-label" for="zfk-' + m.id + '">Kombination mit PIK 1–4 (+0,2)</label></div>' +
(pik.flaeche_je_einheit ? '<div class="input-group input-group-sm ms-auto" style="width:13rem"><span class="input-group-text">Lerchenfenster</span><input type="number" class="form-control" data-f="params.einheiten" min="0" step="1" value="' + esc(p.einheiten ?? '') + '"></div>' : '') + '</div><div class="text-muted small mt-1">Bezug: ' + esc(pik.bezug) + '. Kein Biotoptypwechsel; Bewertung = Bezugsfläche × Bonusfaktor.</div>';
}
case 'F6': {
return '<div class="row g-2"><div class="col-7"><label class="form-label">Kategorie</label><select class="form-select form-select-sm" data-f="params.kwv">' + optList(FK.wiedervernetzung.kategorien, p.kwv || '1', k => k.key, k => k.name) + '</select></div><div class="col-5"><label class="form-label">Form</label><select class="form-select form-select-sm" data-f="params.form">' + optList(Object.keys(FK.wiedervernetzung.formen), p.form || 'flaechig', k => k, k => FK.wiedervernetzung.formen[k]) + '</select></div></div><div class="text-muted small mt-1">Zusätzlich Funktionsherstellung Biotopverbund (FAF nach Bewertungsstufe) auf der Zielfläche eintragen; Retention und bioklimatischer Ausgleich nicht zulässig.</div>';
}
case 'F7': {
return '<div class="row g-2 align-items-end">' +
'<div class="col-4"><label class="form-label">Fläche Querungsanlage (UE)</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.gbf_ue" min="0" value="' + esc(p.gbf_ue ?? '') + '"><span class="input-group-text">m²</span></div></div>' +
'<div class="col-8"><div class="form-check form-check-inline small mt-3"><input class="form-check-input" type="checkbox" data-f="params.gewaesser" data-rerender="1" id="gew-' + m.id + '"' + (p.gewaesser ? ' checked' : '') + '><label class="form-check-label" for="gew-' + m.id + '">gewässergebunden wandernde Zielart (Fischotter …)</label></div>' +
(!p.gewaesser ? '<div class="form-check form-check-inline small"><input class="form-check-input" type="checkbox" data-f="params.grossraeumig" id="gross-' + m.id + '"' + (p.grossraeumig ? ' checked' : '') + '><label class="form-check-label" for="gross-' + m.id + '">großräumig wandernd</label></div>' : '') + '</div>' +
'<div class="col-3"><label class="form-label">BQ Breite Querung</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.bq" min="0" value="' + esc(p.bq ?? '') + '"><span class="input-group-text">m</span></div></div>' +
(p.gewaesser ? '<div class="col-3"><label class="form-label">hS hindernisfreie Strecke</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.hs" min="0" max="4000" value="' + esc(p.hs ?? '') + '"><span class="input-group-text">m</span></div></div>' :
'<div class="col-3"><label class="form-label">LLZ Länge Leiteinrichtung</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.llz" min="0" value="' + esc(p.llz ?? '') + '"><span class="input-group-text">m</span></div></div><div class="col-3"><label class="form-label">LKB Korridor bis Barriere</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.lkb" min="0" value="' + esc(p.lkb ?? '') + '"><span class="input-group-text">m</span></div></div>') +
'<div class="col-3"><label class="form-label">p Anteil geeigneter Habitate</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.p" min="0" max="100" value="' + esc(p.p ?? '') + '"><span class="input-group-text">%</span></div></div>' +
'<div class="col-12 small">anzurechnende Fläche EE aF = <b data-w="af">0</b> m² · <span class="text-muted">' + esc(FK.wiedervernetzung.technisch.hinweis) + '</span></div></div>';
}
case 'F8': {
const grm = FK.fliessgewaesser.massnahmen.find(x => x.key === p.grm) || FK.fliessgewaesser.massnahmen[0];
return '<div class="row g-2 align-items-end"><div class="col-8"><label class="form-label">Gewässerrevitalisierungsmaßnahme (Anlage 6.6)</label><select class="form-select form-select-sm" data-f="params.grm" data-rerender="1">' + optList(FK.fliessgewaesser.massnahmen, grm.key, x => x.key, x => x.key + ' ' + x.name) + '</select></div>' +
'<div class="col-4"><label class="form-label">Gewässertyp</label><select class="form-select form-select-sm" data-f="params.gewtyp"><option value="bach"' + (p.gewtyp !== 'fluss' ? ' selected' : '') + '>Bach</option><option value="fluss"' + (p.gewtyp === 'fluss' ? ' selected' : '') + '>Fluss</option></select></div>' +
(grm.bonus ? '<div class="col-12"><div class="form-check small"><input class="form-check-input" type="checkbox" data-f="params.bonus" id="bfr-' + m.id + '"' + (p.bonus !== false ? ' checked' : '') + '><label class="form-check-label" for="bfr-' + m.id + '">Bonusfaktor Gewässerrevitalisierung BFR ' + fmtF(FK.fliessgewaesser.bfr) + ' ansetzen' + (grm.bonus_hinweis ? ' – ' + esc(grm.bonus_hinweis) : '') + '</label></div></div>' : '') +
(grm.plus_wp ? '<div class="col-12 text-muted small">Planungswert = Ausgangsbiotoptyp + ' + grm.plus_wp + ' WP/m² (Zielbiotoptyp nicht erforderlich).</div>' : '') +
(grm.faf.length ? '<div class="col-12"><button class="btn btn-xs btn-outline-primary btn-grm-faf"><i class="bi bi-list-check"></i> Funktionsaufwertungsfaktoren des Steckbriefs übernehmen</button> <span class="text-muted small">' + grm.faf.map(f => FUNK_ALL.find(x => x.key === f.funktion).name + ' ' + f.afb + ' ' + fmtF(p.gewtyp === 'fluss' ? f.fluss : f.bach) + ' (' + f.bezug + ')').join('; ') + '</span></div>' : '') + '</div>';
}
case 'F9': {
const w = FK.wald.massnahmen.find(x => x.key === p.wald) || FK.wald.massnahmen[0];
return '<label class="form-label">Maßnahme im Wald (Anlage 6.7)</label><select class="form-select form-select-sm" data-f="params.wald" data-rerender="1">' + optList(FK.wald.massnahmen, w.key, x => x.key, x => x.name) + '</select>' +
'<div class="row g-2 mt-1 align-items-end"><div class="col-8"><label class="form-label">Bonusfaktor Wald (× Fläche)</label>' + (w.bfn_optionen ? '<select class="form-select form-select-sm" data-f="params.bfn">' + optList(w.bfn_optionen, p.bfn ?? w.bfn, o => o.bfn, o => fmtF(o.bfn) + ' – ' + o.text) + '</select>' : '<input type="number" class="form-control form-control-sm" data-f="params.bfn" step="0.5" value="' + esc(p.bfn ?? w.bfn) + '">') + '</div><div class="col-4 small text-muted">Ziel: ' + w.ziel.join(', ') + '</div></div><div class="text-muted small mt-1">' + esc(w.faf_hinweis) + '</div>';
}
case 'F10': {
const w = FK.weitere.massnahmen.find(x => x.key === p.art) || FK.weitere.massnahmen[0];
return '<div class="row g-2 align-items-end"><div class="col-8"><label class="form-label">Maßnahme (Anlage 6.8)</label><select class="form-select form-select-sm" data-f="params.art" data-rerender="1">' + optList(FK.weitere.massnahmen, w.key, x => x.key, x => x.name) + '</select></div>' +
'<div class="col-4"><label class="form-label">Bonusfaktor BFN (Regel ' + fmtF(w.bfn) + ', max. ' + fmtF(w.bfn_max) + ')</label><input type="number" class="form-control form-control-sm" data-f="params.bfn" step="0.5" min="0" max="' + w.bfn_max + '" placeholder="' + w.bfn + '" value="' + esc(p.bfn ?? '') + '"></div></div><div class="text-muted small mt-1">' + esc(FK.weitere.hinweis) + '</div>';
}
case 'F11':
return '<div class="row g-2"><div class="col-6"><label class="form-label">Fläche Ausgangszustand (sichtbare Oberfläche)</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.gbf_a" min="0" value="' + esc(p.gbf_a ?? '') + '"><span class="input-group-text">m²</span></div></div><div class="col-6"><label class="form-label">Fläche Planungszustand</label><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="params.gbf_p" min="0" value="' + esc(p.gbf_p ?? '') + '"><span class="input-group-text">m²</span></div></div></div><div class="text-muted small mt-1">' + esc(FK.trockenmauer.hinweis) + '</div>';
}
return '';
}
function funkRows(m) {
return (m.funktionen || []).map(k => '<div class="faf-row row g-1 align-items-end" data-kind="mf" data-id="' + k.id + '">' +
'<div class="col-4"><select class="form-select form-select-sm" data-f="funktion">' + funkOpts(k.funktion, FUNK_ALL) + '</select></div>' +
'<div class="col-1"><select class="form-select form-select-sm px-1" data-f="ewb"><option' + (k.ewb === 'UE' ? ' selected' : '') + '>UE</option><option' + (k.ewb === 'EE' ? ' selected' : '') + '>EE</option></select></div>' +
'<div class="col-2"><select class="form-select form-select-sm" data-f="bsf">' + stufenOpts(k.bsf) + '</select></div>' +
'<div class="col-1"><select class="form-select form-select-sm px-1" data-f="afb">' + optList(['FH', 'FE', 'FV', 'FS'], k.afb, s => s, s => s) + '</select></div>' +
'<div class="col-2"><div class="input-group input-group-sm"><input type="number" class="form-control" data-f="gbf" placeholder="m²" value="' + esc(k.gbf ?? '') + '"><button class="btn btn-outline-secondary btn-gbf-copy px-1" title="Maßnahmenfläche übernehmen"><i class="bi bi-arrow-down-left"></i></button></div></div>' +
'<div class="col-1"><input type="number" class="form-control form-control-sm px-1" data-f="faktor_override" step="0.1" placeholder="' + (BIL.faktorZeile(Object.assign({}, k, { faktor_override: '' })) ?? '') + '" value="' + esc(k.faktor_override ?? '') + '" title="Faktor (Tab. 4-1 oder Steckbrief)"></div>' +
'<div class="col-1 text-end text-nowrap"><b data-w="wp">0</b><button class="btn btn-xs btn-outline-danger btn-del ms-1"><i class="bi bi-x"></i></button></div>' +
(k.bez ? '<div class="col-12 text-muted small">' + esc(k.bez) + '</div>' : '') + '</div>').join('');
}
function mCard(m) {
const ti = typInfo(m.typ);
const p = m.params || {};
const ohneBT = m.typ === 'F5' || m.typ === 'F7';
const grm = m.typ === 'F8' ? FK.fliessgewaesser.massnahmen.find(x => x.key === p.grm) : null;
return '<div class="card card-m" data-kind="m" data-id="' + m.id + '"><div class="card-body">' +
'<div class="d-flex align-items-center gap-2 mb-1"><span class="badge text-bg-success">M' + m.id + '</span><span class="badge bg-secondary-subtle text-dark badge-typ">' + esc(ti.kurz) + '</span>' +
'<input class="form-control form-control-sm" data-f="name" value="' + esc(m.name) + '" placeholder="Bezeichnung der Kompensationsmaßnahme">' +
'<div class="btn-group btn-group-sm ms-auto flex-shrink-0"><button class="btn btn-outline-secondary btn-zoom" title="In Karte zeigen"' + (m.fid ? '' : ' disabled') + '><i class="bi bi-crosshair"></i></button><button class="btn btn-outline-secondary btn-redraw" title="Fläche (neu) zeichnen"><i class="bi bi-pentagon"></i></button><button class="btn btn-outline-danger btn-del"><i class="bi bi-trash"></i></button></div></div>' +
'<div class="mb-2">' + paramsHtml(m) + '</div>' +
(ohneBT ? '' : '<div class="row g-2">' +
'<div class="col-6"><label class="form-label">Ausgangszustand</label>' + btSelect('bt_ausgang', m.bt_ausgang, 'bw').replace('<select ', '<select data-value="' + esc(m.bt_ausgang || '') + '" ') +
'<div class="d-flex align-items-center gap-1 mt-1"><button class="btn btn-xs btn-outline-secondary btn-aas" data-bt="bt_ausgang" data-aas-field="aas_ausgang" data-bestand="1">AAS <span data-aas="aas_ausgang">' + aasText(m.aas_ausgang) + '</span></button><button class="btn btn-xs btn-outline-secondary btn-btinfo" data-bt="bt_ausgang"><i class="bi bi-info-circle"></i></button><span class="ms-auto small">BW <b class="badge text-bg-light wert-badge" data-w="bw">–</b></span></div></div>' +
'<div class="col-6"><label class="form-label">Zielzustand (nach 25 Jahren)</label>' + (grm && grm.plus_wp ? '<div class="form-control form-control-sm bg-light">Ausgangsbiotoptyp + ' + grm.plus_wp + ' WP/m²</div><div class="mt-1 small text-end">PW <b class="badge text-bg-light wert-badge" data-w="pw">–</b></div>' :
btSelect('bt_ziel', m.bt_ziel, 'pw').replace('<select ', '<select data-value="' + esc(m.bt_ziel || '') + '" ') +
'<div class="d-flex align-items-center gap-1 mt-1"><button class="btn btn-xs btn-outline-secondary btn-aas" data-bt="bt_ziel" data-aas-field="aas_ziel">AAS <span data-aas="aas_ziel">' + aasText(m.aas_ziel) + '</span></button><button class="btn btn-xs btn-outline-secondary btn-btinfo" data-bt="bt_ziel"><i class="bi bi-info-circle"></i></button><span class="ms-auto small">PW <b class="badge text-bg-light wert-badge" data-w="pw">–</b></span></div>') + '</div></div>') +
(m.typ === 'F11' || m.typ === 'F7' ? '' : '<div class="d-flex align-items-center gap-2 mt-2 small flex-wrap"><label class="form-label mb-0">' + (m.typ === 'F5' ? 'Bezugsfläche' : 'Maßnahmenfläche') + '</label><div class="input-group input-group-sm" style="width:12rem"><input type="number" class="form-control" data-f="gbf" min="0" step="1" value="' + esc(m.gbf ?? '') + '"' + (p.baum ? ' readonly' : '') + '><span class="input-group-text">m²</span><button class="btn btn-outline-secondary btn-gbf-map" title="Fläche aus Karte übernehmen"' + (m.fid ? '' : ' disabled') + '><i class="bi bi-arrow-repeat"></i></button></div><span class="text-muted" data-w="gbfinfo">' + (m.fid ? (m.gbf_manuell ? 'manuell' : 'aus Karte') : 'ohne Geometrie') + '</span><span class="ms-auto" data-w="s1info"></span></div>') +
'<div class="d-flex align-items-center mt-2"><span class="small fw-semibold">Auf-/Abwertung von Werten und Funktionen besonderer Bedeutung (Anlage 4)</span><button class="btn btn-xs btn-outline-primary ms-auto btn-mf-add"><i class="bi bi-plus-lg"></i> Funktion</button></div>' +
'<div class="small"><div class="row g-1 text-muted" style="font-size:.7rem"><div class="col-4">Funktion</div><div class="col-1">EWB</div><div class="col-2">Stufe</div><div class="col-1">Art</div><div class="col-2">Fläche m²</div><div class="col-1">Faktor</div><div class="col-1 text-end">WP</div></div><div class="list-mf">' + funkRows(m) + '</div></div>' +
'<div class="mt-2 small d-flex flex-wrap gap-3 border-top pt-1"><span>Biotoptypen S1 <b data-w="S1">0</b></span><span>Funktionen S2 <b data-w="S2">0</b></span><span>Abwertung S3 <b data-w="S3">0</b></span><span>Landschaft S4 <b data-w="S4">0</b></span><span class="ms-auto">L1 <b class="text-success" data-w="L1">0</b> · L2 <b class="text-success" data-w="L2">0</b> WP</span></div>' +
'<div class="small warn" data-w="warn"></div>' +
'</div></div>';
}
function renderM() {
const $l = $('#list-m').empty();
if (!P.massnahmen.length) $l.html('<div class="text-muted small border rounded p-2">Noch keine Kompensationsmaßnahmen. Maßnahmentyp wählen und Fläche zeichnen oder Zeile anlegen.</div>');
P.massnahmen.forEach(m => $l.append(mCard(m)));
initSelects($l);
$('#badge-komp').text(P.massnahmen.length);
}
function renderMCard(m) {
const $old = $('.card[data-kind="m"][data-id="' + m.id + '"]');
$old.find('select.bt-select').each(function () { try { $(this).selectpicker('destroy'); } catch (e) { } });
const $new = $(mCard(m)); $old.replaceWith($new); initSelects($new);
}
function addM(typ, fid) {
const m = { id: uid(), fid: fid || null, typ, name: '', bt_ausgang: '', aas_ausgang: [], bt_ziel: '', aas_ziel: [], gbf: '', gbf_manuell: false, params: {}, funktionen: [] };
// Voreinstellungen je Typ
if (typ === 'F4') { m.params.pik = '1'; m.bt_ausgang = '10.01.200'; }
if (typ === 'F5') { m.params.pik = '10a'; }
if (typ === 'F6') { m.params.kwv = '1'; m.params.form = 'flaechig'; }
if (typ === 'F8') { m.params.grm = '1a'; m.params.gewtyp = 'bach'; }
if (typ === 'F9') { m.params.wald = 'naturwaldzelle'; m.params.bfn = 3; m.bt_ziel = '01.11.200'; }
if (typ === 'F10') { m.params.art = 'moor'; }
if (typ === 'F11') { m.bt_ausgang = FK.trockenmauer.ausgang; m.bt_ziel = FK.trockenmauer.ziel; }
if (typ === 'F3') { m.params.kbh = 0; }
P.massnahmen.push(m);
if (fid) { KARTE.setProps(fid, { label: 'M' + m.id, ref: 'm:' + m.id }); syncArea(m); }
renderM(); refreshCalc(); scheduleSave();
return m;
}
/* ----------------------------------------------- generische Events */
function recOf($el) {
const $c = $el.closest('[data-kind]'); const kind = $c.data('kind'); const id = $c.data('id');
if (kind === 'ef') return { kind, rec: findEF(id), $card: $c };
if (kind === 'fk') return { kind, rec: findFK(id), $card: $c };
if (kind === 'm') return { kind, rec: findM(id), $card: $c };
if (kind === 'la') return { kind, rec: P.eingriff.landschaft.aufwertungen.find(a => a.id == id), $card: $c, parent: P.eingriff.landschaft };
if (kind === 'mf') { const $m = $c.closest('[data-kind="m"]'); const m = findM($m.data('id')); return { kind, rec: (m.funktionen || []).find(k => k.id == id), $card: $c, parent: m, $mcard: $m }; }
return null;
}
function setPath(obj, path, val) {
const parts = path.split('.'); let o = obj;
for (let i = 0; i < parts.length - 1; i++) { o[parts[i]] = o[parts[i]] || {}; o = o[parts[i]]; }
o[parts[parts.length - 1]] = val;
}
$(document).on('change', '#list-ef [data-f], #list-fk [data-f], #list-m [data-f], #list-land-aufw [data-f]', function () {
const r = recOf($(this)); if (!r || !r.rec) return;
const f = $(this).data('f');
const val = this.type === 'checkbox' ? this.checked : this.value;
setPath(r.rec, f, val);
if (f === 'gbf') r.rec.gbf_manuell = true;
if (f === 'kronen_dm' && r.rec.fid) { KARTE.setProps(r.rec.fid, { kronen_dm: num(val) }); KARTE.refresh(); r.rec.gbf_manuell = false; syncArea(r.rec); r.$card.find('[data-f="gbf"]').val(r.rec.gbf); }
if (f === 'bez' && r.rec.fid) { KARTE.setProps(r.rec.fid, { label: (r.kind === 'ef' ? 'E' : 'F') + r.rec.id + (val ? ' ' + val : '') }); KARTE.refresh(); }
if (f === 'name' && r.rec.fid) { KARTE.setProps(r.rec.fid, { label: 'M' + r.rec.id + (val ? ' ' + val : '') }); KARTE.refresh(); }
if (f.startsWith('bt_')) { // AAS bereinigen, die für den neuen Typ nicht zulässig sind
const aasField = f.replace('bt_', 'aas_'); const t = byBtc(val); const bestandSeite = (f === 'bt_bestand' || f === 'bt_ausgang');
if (t) { r.rec[aasField] = (r.rec[aasField] || []).filter(a => a.key === 'frei' || (bestandSeite ? t.aas_b : t.aas_p).includes(a.nr)); }
r.$card.find('[data-aas="' + aasField + '"]').text(aasText(r.rec[aasField]));
}
if (r.kind === 'm' && f.startsWith('params.') && r.rec.params.baum) {
const je = r.rec.params.baumart === 'obst' ? FK.einzelbaum.kronentrauf_obst_m2 : FK.einzelbaum.kronentrauf_laub_m2;
r.rec.gbf = num(r.rec.params.anzahl) * je; r.rec.gbf_manuell = true;
if (!r.rec.bt_ausgang) r.rec.bt_ausgang = FK.einzelbaum.ausgangs_btc;
}
if (r.kind === 'm' && f === 'params.pik') { const pik = FK.pik.massnahmen.find(x => x.key === val); if (pik && pik.ausgang && pik.ausgang.length === 1) r.rec.bt_ausgang = pik.ausgang[0]; if (pik && pik.ziel && pik.ziel.length === 1) r.rec.bt_ziel = pik.ziel[0]; }
if (r.kind === 'm' && f === 'params.wald') { const w = FK.wald.massnahmen.find(x => x.key === val); if (w) { r.rec.bt_ziel = w.ziel[0]; r.rec.params.bfn = w.bfn; } }
if (r.kind === 'm' && f === 'params.art') { const w = FK.weitere.massnahmen.find(x => x.key === val); if (w) r.rec.params.bfn = ''; }
if (r.kind === 'm' && f === 'params.gewtyp') { // Steckbrief-Faktoren neu setzen
(r.rec.funktionen || []).filter(k => k.grm).forEach(k => { const grm = FK.fliessgewaesser.massnahmen.find(x => x.key === r.rec.params.grm); const src = grm && grm.faf.find(x => x.funktion === k.funktion && x.afb === k.afb && x.bezug === k.bez); if (src) k.faktor_override = val === 'fluss' ? src.fluss : src.bach; });
}
if ($(this).data('rerender')) { if (r.kind === 'm') renderMCard(r.rec); }
if (r.kind === 'mf' && f === 'funktion') { }
refreshCalc(); scheduleSave();
});
$(document).on('click', '.btn-del', function () {
const r = recOf($(this)); if (!r || !r.rec) return;
if (r.kind === 'ef') { if (r.rec.fid) KARTE.removeFeature(r.rec.fid); P.eingriff.flaechen = P.eingriff.flaechen.filter(x => x !== r.rec); renderEF(); }
else if (r.kind === 'fk') { if (r.rec.fid) KARTE.removeFeature(r.rec.fid); P.eingriff.funktionen = P.eingriff.funktionen.filter(x => x !== r.rec); renderFK(); }
else if (r.kind === 'm') { if (!confirm('Maßnahme M' + r.rec.id + ' löschen?')) return; if (r.rec.fid) KARTE.removeFeature(r.rec.fid); P.massnahmen = P.massnahmen.filter(x => x !== r.rec); renderM(); }
else if (r.kind === 'la') { r.parent.aufwertungen = r.parent.aufwertungen.filter(x => x !== r.rec); renderLand(); }
else if (r.kind === 'mf') { r.parent.funktionen = r.parent.funktionen.filter(x => x !== r.rec); r.$card.remove(); }
refreshCalc(); scheduleSave();
});
$(document).on('click', '.btn-zoom', function () { const r = recOf($(this)); if (r && r.rec && r.rec.fid) { KARTE.zoomTo(r.rec.fid); KARTE.selectFeature(r.rec.fid); } });
$(document).on('click', '.btn-redraw', function () {
const r = recOf($(this)); if (!r) return;
const rolle = r.kind === 'ef' ? 'eingriff' : r.kind === 'fk' ? 'funktionsraum' : 'kompensation';
startDraw({ kind: r.kind, id: r.rec.id }, rolle, 'Polygon');
});
$(document).on('click', '.btn-gbf-map', function () { const r = recOf($(this)); if (!r || !r.rec.fid) return; r.rec.gbf_manuell = false; syncArea(r.rec); r.$card.find('[data-f="gbf"]').val(r.rec.gbf); refreshCalc(); scheduleSave(); });
$(document).on('click', '.btn-gbf-copy', function () { const r = recOf($(this)); if (!r) return; r.rec.gbf = r.parent.gbf; r.$card.find('[data-f="gbf"]').val(r.rec.gbf); refreshCalc(); scheduleSave(); });
$(document).on('click', '.btn-aas', function () {
const r = recOf($(this)); if (!r) return;
openAas({ rec: r.rec, $card: r.$card, btField: $(this).data('bt'), aasField: $(this).data('aasField'), bestandSeite: !!$(this).data('bestand') });
});
$(document).on('click', '.btn-btinfo', function () { const r = recOf($(this)); if (r) showBtInfo(r.rec[$(this).data('bt')]); });
$(document).on('click', '.btn-mf-add', function () {
const r = recOf($(this)); if (!r) return;
r.rec.funktionen = r.rec.funktionen || [];
r.rec.funktionen.push({ id: uid(), funktion: FUNK_ALL[0].key, ewb: 'UE', bsf: 'mittel', afb: 'FH', gbf: r.rec.gbf || '', faktor_override: '', bez: '' });
r.$card.find('.list-mf').html(funkRows(r.rec)); refreshCalc(); scheduleSave();
});
$(document).on('click', '.btn-grm-faf', function () {
const r = recOf($(this)); if (!r) return; const p = r.rec.params;
const grm = FK.fliessgewaesser.massnahmen.find(x => x.key === p.grm); if (!grm) return;
r.rec.funktionen = (r.rec.funktionen || []).filter(k => !k.grm);
grm.faf.forEach(f => r.rec.funktionen.push({ id: uid(), grm: grm.key, funktion: f.funktion, ewb: 'UE', bsf: 'hoch', afb: f.afb, gbf: r.rec.gbf || '', faktor_override: p.gewtyp === 'fluss' ? f.fluss : f.bach, bez: f.bezug }));
r.$card.find('.list-mf').html(funkRows(r.rec)); refreshCalc(); scheduleSave();
toast('Faktoren übernommen – Bezugsflächen je Zeile prüfen/eintragen.', 'primary');
});
$('#btn-ef-add').on('click', () => addEF(null));
$('#btn-ef-draw').on('click', () => startDraw({ kind: 'ef' }, 'eingriff', 'Polygon'));
$('#btn-fk-add').on('click', () => addFK(null));
$('#btn-fk-draw').on('click', () => startDraw({ kind: 'fk' }, 'funktionsraum', 'Polygon'));
$('#btn-m-add').on('click', () => addM($('#sel-mtyp').val(), null));
$('#btn-m-draw').on('click', () => startDraw({ kind: 'm', typ: $('#sel-mtyp').val() }, 'kompensation', 'Polygon'));
/* ------------------------------------------------- Karte ↔ Daten */
function syncArea(rec) {
if (!rec.fid) return;
const a = KARTE.area(rec.fid);
if (a !== null && !rec.gbf_manuell) rec.gbf = Math.round(a);
}
function startDraw(ctx, rolle, type) {
drawCtx = ctx;
KARTE.startDraw(rolle, type, ctx);
$('#map-hint').text(type === 'Point' ? 'Punkt in der Karte setzen (Esc = abbrechen).' : 'Fläche in der Karte zeichnen – Klick je Stützpunkt, Doppelklick beendet (Esc = abbrechen).');
$('#btn-draw-cancel').prop('hidden', false);
}
function onDrawEnd(feature, rolle, ctx) {
const fid = feature.get('fid');
ctx = ctx || drawCtx || {};
let rec = null;
if (ctx.kind === 'ef') {
rec = ctx.id ? findEF(ctx.id) : null;
if (rec) { if (rec.fid) KARTE.removeFeature(rec.fid); rec.fid = fid; rec.gbf_manuell = false; KARTE.setProps(fid, { label: 'E' + rec.id + (rec.bez ? ' ' + rec.bez : ''), ref: 'ef:' + rec.id, kronen_dm: rec.kronen_dm || undefined }); syncArea(rec); renderEF(); }
else rec = addEF(fid, rolle);
showTab('tab-eingriff');
} else if (ctx.kind === 'fk') {
rec = ctx.id ? findFK(ctx.id) : null;
if (rec) { if (rec.fid) KARTE.removeFeature(rec.fid); rec.fid = fid; rec.gbf_manuell = false; KARTE.setProps(fid, { label: 'F' + rec.id, ref: 'fk:' + rec.id }); syncArea(rec); renderFK(); }
else rec = addFK(fid);
showTab('tab-eingriff');
} else if (ctx.kind === 'm') {
rec = ctx.id ? findM(ctx.id) : null;
if (rec) { if (rec.fid) KARTE.removeFeature(rec.fid); rec.fid = fid; rec.gbf_manuell = false; KARTE.setProps(fid, { label: 'M' + rec.id + (rec.name ? ' ' + rec.name : ''), ref: 'm:' + rec.id }); syncArea(rec); renderM(); }
else rec = addM(ctx.typ || 'F2', fid);
showTab('tab-komp');
} else if (ctx.kind === 'standort') {
const L = P.eingriff.landschaft;
if (L.standort_fid) KARTE.removeFeature(L.standort_fid);
L.standort_fid = fid; KARTE.setProps(fid, { label: 'Standort', ref: 'land' }); updateZonen(); renderLand();
}
drawCtx = null;
$('#map-hint').text('Fläche gezeichnet. Stützpunkte mit dem Stift-Werkzeug bearbeiten.');
refreshCalc(); scheduleSave();
if (rec && rec.id) highlightCard(ctx.kind === 'm' ? 'm' : ctx.kind, rec.id);
}
function refOf(fid) {
const f = KARTE.getFeature(fid); if (!f) return null;
const ref = f.get('ref') || ''; const [kind, id] = ref.split(':');
return { kind, id };
}
function highlightCard(kind, id) {
$('.card.selected').removeClass('selected');
const $c = $('.card[data-kind="' + kind + '"][data-id="' + id + '"]');
if ($c.length) { $c.addClass('selected'); $c[0].scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }
}
function onSelect(fid) {
selectedFid = fid;
$('.card.selected').removeClass('selected');
if (!fid) return;
const r = refOf(fid); if (!r) return;
if (r.kind === 'ef' || r.kind === 'fk') showTab('tab-eingriff');
if (r.kind === 'm') showTab('tab-komp');
if (r.kind === 'land') { showTab('tab-eingriff'); return; }
setTimeout(() => highlightCard(r.kind, r.id), 150);
}
function onModify(ids) {
ids.forEach(fid => {
const r = refOf(fid); if (!r) return;
const rec = r.kind === 'ef' ? findEF(r.id) : r.kind === 'fk' ? findFK(r.id) : r.kind === 'm' ? findM(r.id) : null;
if (rec) { syncArea(rec); $('.card[data-kind="' + r.kind + '"][data-id="' + rec.id + '"] [data-f="gbf"]').val(rec.gbf); }
if (r.kind === 'land') updateZonen();
});
refreshCalc(); scheduleSave();
}
function onDeleteGeometry(fid) {
const r = refOf(fid); if (!r) return;
const rec = r.kind === 'ef' ? findEF(r.id) : r.kind === 'fk' ? findFK(r.id) : r.kind === 'm' ? findM(r.id) : null;
if (!confirm('Geometrie in der Karte löschen? Der Eintrag bleibt mit manueller Fläche erhalten.')) return;
KARTE.removeFeature(fid);
if (rec) { rec.fid = null; rec.gbf_manuell = true; }
if (r.kind === 'land') { P.eingriff.landschaft.standort_fid = null; KARTE.clearZonen(); renderLand(); }
renderEF(); renderFK(); renderM(); refreshCalc(); scheduleSave();
}
$('#map-tools [data-tool]').on('click', function () {
$('#map-tools [data-tool]').removeClass('active'); $(this).addClass('active');
KARTE.setTool($(this).data('tool'));
$('#map-hint').text({ select: 'Auswählen: Klick auf eine Fläche wählt den Eintrag.', modify: 'Bearbeiten: Fläche anklicken, dann Stützpunkte ziehen.', delete: 'Löschen: Klick auf eine Fläche entfernt ihre Geometrie.' }[$(this).data('tool')]);
});
$('#btn-draw-cancel').on('click', () => { KARTE.cancelDraw(); });
$('#sel-basis').on('change', function () { KARTE.setBase(this.value); });
$('#chk-flst').on('change', function () { KARTE.setFlst(this.checked); });
$('#btn-suche').on('click', doSearch); $('#inp-suche').on('keydown', e => { if (e.key === 'Enter') doSearch(); });
function doSearch() {
const q = $('#inp-suche').val().trim(); if (!q) return;
KARTE.search(q).done(res => { if (!res.length) { toast('Kein Treffer.', 'warning'); return; } KARTE.centerLonLat(+res[0].lon, +res[0].lat, 11); toast(res[0].display_name, 'secondary'); }).fail(() => toast('Suche nicht erreichbar.', 'danger'));
}
/* ------------------------------------------------- Berechnungen */
function refreshCalc() {
const G = BIL.calcGesamt(P);
const f1 = G.f1;
// Eingriffsflächen
f1.bestand.forEach((b, i) => {
const p = f1.planung[i];
const $c = $('.card[data-kind="ef"][data-id="' + b.id + '"]');
$c.find('[data-w="bw"]').text(b.wert === null ? '–' : b.wert).attr('title', BIL.stufe(b.wert));
$c.find('[data-w="pw"]').text(p.wert === null ? '–' : p.wert);
$c.find('[data-w="wpb"]').text(fmtWP(b.wp)); $c.find('[data-w="wpp"]').text(fmtWP(p.wp)); $c.find('[data-w="diff"]').text(fmtWP(b.wp - p.wp));
});
f1.fkRows.forEach(k => {
const $c = $('.card[data-kind="fk"][data-id="' + k.id + '"]');
$c.find('[data-w="faktor"]').text(fmtF(BIL.isMinderung(k.afb) ? BIL.fmf(k.bsf, k.afb) : BIL.faf(k.bsf, k.afb)));
$c.find('[data-w="wp"]').text(fmtWP(k.wp)); $c.find('[data-w="seite"]').text(k.seite === 'A' ? 'Wertminderung A2' : 'Aufwertung P2');
const fk = FK.funktionen.find(x => x.key === k.funktion); $c.find('[data-w="funkkurz"]').text(fk ? fk.schutzgut + ' · ' + fk.kurz : '');
});
// Landschaftsbild
const land = f1.land; const $b = $('#box-land');
land.zonen.forEach(z => { $b.find('[data-w="zname' + z.nr + '"]').text(z.name); $b.find('[data-w="zwp' + z.nr + '"]').text(fmtWP(z.stufen.reduce((s, x) => s + x.wp, 0))); if (!P.eingriff.landschaft['fwz' + z.nr + '_manuell']) $b.find('[data-land="fwz' + z.nr + '"]').val(Math.round(z.fwz)); });
$b.find('[data-w="A3"]').text(fmtWP(land.A3));
if (land.ersatz) { land.ersatz.rows.forEach(r => $b.find('[data-w="esz-' + r.stufe.replace(' ', '_') + '"]').text(fmtEur(r.esz))); $b.find('[data-w="E1"]').text(fmtEur(land.ersatz.E1)); $b.find('[data-w="ablsum"]').text('Summe Anteile: ' + land.ersatz.ablSumme + ' %' + (Math.round(land.ersatz.ablSumme) !== 100 && land.ersatz.ablSumme ? ' (≠ 100 %)' : '') + (land.ersatz.korr ? ' · Repowering ' + fmtEur(land.ersatz.korr) : '')); }
land.aufwertungen.forEach(a => { const $r = $('[data-kind="la"][data-id="' + a.id + '"]'); $r.find('[data-w="faktor"]').text(fmtF(BIL.faf(a.bsf, a.afb))); $r.find('[data-w="wp"]').text(fmtWP(a.wp)); });
$('#box-f1-summe').html('<div class="d-flex flex-wrap gap-3"><span>A1 Biotoptypen <b>' + fmtWP(f1.A1) + '</b></span><span>A2 Funktionen <b>' + fmtWP(f1.A2) + '</b></span><span>A3 Landschaftsbild <b>' + fmtWP(f1.A3) + '</b></span><span class="text-muted">|</span><span>P1 <b>' + fmtWP(f1.P1) + '</b></span><span>P2 <b>' + fmtWP(f1.P2) + '</b></span><span>P3 <b>' + fmtWP(f1.P3) + '</b></span></div>' +
'<div class="d-flex flex-wrap gap-4 mt-1"><span>Kompensationsbedarf Naturhaushalt <b>B1 = ' + fmtWP(f1.B1) + ' WP</b></span><span>Landschaftsbild <b>B2 = ' + fmtWP(f1.B2) + ' WP</b></span>' + (f1.B1 <= 0 && f1.B2 <= 0 && (f1.A1 || f1.A3) ? '<span class="ok">kein Kompensationserfordernis</span>' : '') + '</div>');
$('#badge-eingriff').text(fmtWP(f1.B1));
// Maßnahmen
G.ms.forEach(({ m, c }) => {
const $c = $('.card[data-kind="m"][data-id="' + m.id + '"]');
$c.find('[data-w="bw"]').text(c.bw === null ? '–' : c.bw); $c.find('[data-w="pw"]').text(c.pw === null ? '–' : c.pw);
['S1', 'S2', 'S3', 'S4', 'L1', 'L2'].forEach(k => $c.find('[data-w="' + k + '"]').text(fmtWP(c[k])));
$c.find('[data-w="af"]').text(fmtM2(c.af || 0));
$c.find('[data-w="s1info"]').html((c.bw !== null && c.pw !== null ? 'Δ ' + signed(BIL.r2(c.diff)) + ' WP/m²' : '') + (c.bonus ? ' · ' + esc(c.bonusName) + ' ' + fmtF(c.bonus) : ''));
c.fRows.forEach(k => { const $r = $c.find('[data-kind="mf"][data-id="' + k.id + '"]'); $r.find('[data-w="wp"]').text(fmtWP(k.wp)); $r.find('[data-f="faktor_override"]').attr('placeholder', BIL.faktorZeile(Object.assign({}, k, { faktor_override: '' })) ?? ''); });
$c.find('[data-w="warn"]').html(c.warn.map(w => '<i class="bi bi-exclamation-triangle"></i> ' + esc(w)).join('<br>'));
});
renderBilanz(G);
if ($('#tab-form').hasClass('active')) renderForm(G);
}
/* --------------------------------------------------------- Bilanz */
function renderBilanz(G) {
const ok1 = G.bil1 <= 0.005, ok2 = G.bil2 <= 0.005;
const kachel = (t, v, cls, sub) => '<div class="col-6 col-xl-4"><div class="card h-100"><div class="card-body p-2"><div class="text-muted small">' + t + '</div><div class="wp-big ' + (cls || '') + '">' + v + '</div>' + (sub ? '<div class="small text-muted">' + sub + '</div>' : '') + '</div></div></div>';
let h = '<h6 class="text-uppercase text-muted small mb-2">Gesamtbilanz (' + (G.mast ? 'F14' : 'F13') + ')</h6><div class="row g-2">' +
kachel('Kompensationsbedarf Naturhaushalt B1', fmtWP(G.B1) + ' WP', '', 'aus F1: (A1 + A2) − (P1 + P2)') +
kachel('Kompensationsleistung Naturhaushalt L1', fmtWP(G.L1) + ' WP', 'text-success', 'aus F2 – F11') +
kachel('Bilanzwert Naturhaushalt B1 − L1', fmtWP(G.bil1) + ' WP', ok1 ? 'ok' : 'warn', ok1 ? 'Eingriff in den Naturhaushalt vollständig kompensiert' : 'Defizit – weitere Kompensation erforderlich');
if (G.mast) {
h += kachel('Ersatzzahlung Landschaftsbild E1 (F12)', fmtEur(G.E1), '', 'mastenartiger Eingriff ab 50 m');
} else {
h += kachel('Kompensationsbedarf Landschaftsbild B2', fmtWP(G.B2) + ' WP', '', 'aus F1: A3 − P3') +
kachel('Kompensationsleistung Landschaftsbild L2', fmtWP(G.L2) + ' WP', 'text-success', 'landschaftsästhetische Funktion aus F2 – F11') +
kachel('Bilanzwert Landschaftsbild B2 − L2', fmtWP(G.bil2) + ' WP', ok2 ? 'ok' : 'warn', ok2 ? 'Landschaftsbild vollständig kompensiert' : 'Defizit');
}
h += '</div>';
if (P.meta.einfachfall) h += '<div class="alert alert-info small mt-3 py-2">Als Einfachfall (Anlage 1) gekennzeichnet: kein gutachterlicher Darlegungsbedarf, vereinfachte verbale Gegenüberstellung genügt.</div>';
h += '<h6 class="text-uppercase text-muted small mt-3 mb-2">Beiträge der Maßnahmen</h6>';
if (!G.ms.length) h += '<div class="text-muted small">Noch keine Maßnahmen.</div>';
else {
h += '<table class="table table-sm small"><thead><tr><th>#</th><th>Maßnahme</th><th>Formblatt</th><th class="text-end">Fläche m²</th><th class="text-end">Δ WP/m²</th><th class="text-end">Bonus</th><th class="text-end">L1</th><th class="text-end">L2</th></tr></thead><tbody>';
G.ms.forEach(({ m, c }) => { h += '<tr><td>M' + m.id + '</td><td>' + esc(m.name || btLabel(m.bt_ziel)) + (c.warn.length ? ' <i class="bi bi-exclamation-triangle warn" title="' + esc(c.warn.join(' ')) + '"></i>' : '') + '</td><td>' + m.typ + '</td><td class="text-end">' + fmtM2(c.bezugsflaeche) + '</td><td class="text-end">' + (c.bw !== null && c.pw !== null ? signed(BIL.r2(c.diff)) : '–') + '</td><td class="text-end">' + (c.bonus ? fmtF(c.bonus) : '–') + '</td><td class="text-end">' + fmtWP(c.L1) + '</td><td class="text-end">' + fmtWP(c.L2) + '</td></tr>'; });
h += '<tr class="fw-semibold"><td colspan="6">Summe</td><td class="text-end">' + fmtWP(G.L1) + '</td><td class="text-end">' + fmtWP(G.L2) + '</td></tr></tbody></table>';
}
if (G.bil1 > 0) {
h += '<div class="small text-muted">Orientierung: Das Defizit von ' + fmtWP(G.bil1) + ' WP entspricht z. B. ' + fmtM2(G.bil1 / 10) + ' m² Aufwertung um 10 WP/m² (Acker → extensive Frischwiese) oder ' + Math.ceil(G.bil1 / (30 * 21)) + ' Laubbäumen (30 m² × PW 21).</div>';
}
$('#box-bilanz').html(h);
}
/* --------------------------------------------------- Formblätter */
function kopf() {
const m = P.meta;
return '<div class="fb-kopf mb-2"><b>' + esc(m.name || '(Vorhaben ohne Bezeichnung)') + '</b>' + (m.vkz ? ' · ' + esc(m.vkz) : '') + (m.ort ? ' · ' + esc(m.ort) : '') + (m.traeger ? ' · Vorhabenträger: ' + esc(m.traeger) : '') + (m.bearbeiter ? ' · Bearbeiter: ' + esc(m.bearbeiter) : '') + (m.datum ? ' · ' + esc(m.datum) : '') + '</div>';
}
const aasMark = list => list && list.length ? (BIL.aasSumme(list) > 0 ? '+' : BIL.aasSumme(list) < 0 ? '−' : '+/−') : '';
const funkName = key => (FUNK_ALL.find(f => f.key === key) || {}).name || key;
function tblBiotop(rows, wertName, art) {
let h = '<table><thead><tr><th>#</th><th>BTC</th><th>BTB</th><th>AAS</th><th class="num">GBF m²</th><th class="num">' + wertName + ' WP/m²</th><th class="num">BFB WP</th></tr></thead><tbody>';
let sum = 0;
rows.forEach(r => { sum += r.wp; h += '<tr><td>E' + r.id + '</td><td>' + esc(r.btc || '–') + aasMark(r.aas) + '</td><td>' + esc(r.bt ? r.bt.name : '') + (r.bez ? ' <i>(' + esc(r.bez) + ')</i>' : '') + '</td><td>' + esc((r.aas || []).map(a => a.key).join(', ')) + '</td><td class="num">' + fmtM2(r.gbf) + '</td><td class="num">' + (r.wert === null ? '–' : r.wert) + '</td><td class="num">' + fmtWP(r.wp) + '</td></tr>'; });
h += '<tr class="sum"><td colspan="6">Summe ' + art + '</td><td class="num">' + fmtWP(sum) + '</td></tr></tbody></table>';
return h;
}
function tblFunk(rows, faktorName, sumName, filterFn) {
const rr = rows.filter(filterFn);
let h = '<table><thead><tr><th>Betroffene Funktion</th><th>EWB</th><th class="num">GBF m²</th><th>BSF</th><th>AFB</th><th class="num">' + faktorName + '</th><th class="num">BFF WP</th></tr></thead><tbody>';
let sum = 0;
if (!rr.length) h += '<tr><td colspan="7" class="text-muted">–</td></tr>';
rr.forEach(k => { sum += k.wp; h += '<tr><td>' + esc(funkName(k.funktion)) + (k.bez || k.bemerkung ? ' <i>(' + esc(k.bez || k.bemerkung) + ')</i>' : '') + '</td><td>' + esc(k.ewb) + '</td><td class="num">' + fmtM2(k.gbf) + '</td><td>' + esc(k.bsf) + '</td><td>' + esc(k.afb) + '</td><td class="num">' + fmtF(k.faktor) + '</td><td class="num">' + fmtWP(k.wp) + '</td></tr>'; });
h += '<tr class="sum"><td colspan="6">' + sumName + '</td><td class="num">' + fmtWP(sum) + '</td></tr></tbody></table>';
return h;
}
function renderForm(G) {
G = G || BIL.calcGesamt(P);
const f1 = G.f1;
let h = '<h5>F1 Ermittlung des Kompensationsbedarfs</h5>' + kopf();
h += '<div class="fw-semibold">Bewertung Ausgangszustand – Naturhaushalt, allgemeine Lebensraumfunktion (Biotoptypen)</div>' + tblBiotop(f1.bestand, 'BW', 'A1');
h += '<div class="fw-semibold">Naturhaushalt – Werte und Funktionen besonderer Bedeutung</div>' + tblFunk(f1.fkRows, 'FMF', 'Summe A2', k => k.seite === 'A');
h += '<div class="fw-semibold">Landschaftsbild – landschaftsästhetische Funktion</div>';
if (f1.land.zonen.length) {
h += '<table><thead><tr><th>Wirkzone</th><th class="num">FWZ m²</th><th class="num">WPK WP/m²</th><th>BSL</th><th class="num">AFE %</th><th class="num">AFE m²</th><th>AFB</th><th class="num">FMF</th><th class="num">BFF WP</th></tr></thead><tbody>';
f1.land.zonen.forEach(z => z.stufen.forEach((s, i) => { h += '<tr>' + (i === 0 ? '<td rowspan="3">' + esc(z.name) + '</td><td rowspan="3" class="num">' + fmtM2(z.fwz) + '</td><td rowspan="3" class="num">' + fmtF(z.wpk) + '</td>' : '') + '<td>' + s.stufe + '</td><td class="num">' + s.pct + '</td><td class="num">' + fmtM2(s.afe) + '</td><td>FS</td><td class="num">' + fmtF(s.fmf) + '</td><td class="num">' + fmtWP(s.wp) + '</td></tr>'; }));
h += '<tr class="sum"><td colspan="8">Summe A3</td><td class="num">' + fmtWP(f1.A3) + '</td></tr></tbody></table>';
} else h += '<table><tbody><tr><td class="text-muted">' + (f1.land.typ.key === 'mast_ab50' ? 'mastenartiger Eingriff ab 50 m – Ersatzzahlung nach F12' : 'keine erhebliche Beeinträchtigung') + '</td></tr><tr class="sum"><td>Summe A3: ' + fmtWP(f1.A3) + '</td></tr></tbody></table>';
h += '<div class="fw-semibold">Bewertung Planungszustand – Naturhaushalt, allgemeine Lebensraumfunktion (Biotoptypen)</div>' + tblBiotop(f1.planung, 'PW', 'P1');
h += '<div class="fw-semibold">Naturhaushalt – Werte und Funktionen besonderer Bedeutung (Aufwertung)</div>' + tblFunk(f1.fkRows, 'FAF', 'Summe P2', k => k.seite === 'P');
h += '<div class="fw-semibold">Landschaftsbild – landschaftsästhetische Funktion (Aufwertung)</div>' + tblFunk(f1.land.aufwertungen.map(a => Object.assign({ funktion: 'landschaft', ewb: 'UE' }, a)), 'FAF', 'Summe P3', () => true);
h += '<table><tbody><tr class="sum"><td>([Summe A1 + A2] – [Summe P1 + P2]) Kompensationsbedarf Naturhaushalt (B1)</td><td class="num">' + fmtWP(f1.B1) + '</td></tr><tr class="sum"><td>(Summe A3 – Summe P3) Kompensationsbedarf landschaftsästhetische Funktion (B2)</td><td class="num">' + fmtWP(f1.B2) + '</td></tr></tbody></table><div class="small text-muted">Bei einem Wert ≤ 0 besteht kein Kompensationserfordernis.</div>';
G.ms.forEach(({ m, c }) => {
const ti = typInfo(m.typ); const p = m.params || {};
h += '<h5>' + m.typ + ' ' + esc(ti.name) + ' – M' + m.id + (m.name ? ' „' + esc(m.name) + '“' : '') + '</h5>';
if (m.typ === 'F7') {
h += '<table><thead><tr><th>BKM</th><th>Betroffene Funktion</th><th>EWB</th><th class="num">GBF m²</th><th class="num">BFV</th><th class="num">BFF WP</th></tr></thead><tbody>' +
'<tr><td>M' + m.id + '</td><td>Biotopverbundfunktion</td><td>UE</td><td class="num">' + fmtM2(p.gbf_ue) + '</td><td class="num">1,8</td><td class="num">' + fmtWP(num(p.gbf_ue) * 1.8) + '</td></tr>' +
'<tr><td></td><td>Biotopverbundfunktion (aF = ' + (p.gewaesser ? '(BQ+20)·hS·p' : '(BQ+LLZ)·LKB·p') + ')</td><td>EE</td><td class="num">' + fmtM2(c.af) + '</td><td class="num">1,4</td><td class="num">' + fmtWP(num(c.af) * 1.4) + '</td></tr>' +
'<tr class="sum"><td colspan="5">Summe 1</td><td class="num">' + fmtWP(c.S1) + '</td></tr></tbody></table>';
} else {
const bonusCols = c.bonus ? '<th class="num">' + esc(c.bonusName) + '</th>' : '';
h += '<div class="fw-semibold">Naturhaushalt – Aufwertung allgemeine Lebensraumfunktion (Biotoptypen)</div><table><thead><tr><th>BKM</th><th>Ausgang BTC</th><th>BTB</th><th class="num">BW</th><th>Ziel BTC</th><th>BTB</th><th class="num">PW</th><th class="num">GBF m²</th>' + bonusCols + '<th class="num">BFB WP</th></tr></thead><tbody>' +
'<tr><td>M' + m.id + '</td><td>' + esc(m.bt_ausgang || '–') + aasMark(m.aas_ausgang) + '</td><td>' + esc((byBtc(m.bt_ausgang) || {}).name || '') + '</td><td class="num">' + (c.bw === null ? '–' : c.bw) + '</td><td>' + esc(m.bt_ziel || (c.grm && c.grm.plus_wp ? 'Ausgang +' + c.grm.plus_wp : '–')) + aasMark(m.aas_ziel) + '</td><td>' + esc((byBtc(m.bt_ziel) || {}).name || '') + '</td><td class="num">' + (c.pw === null ? '–' : c.pw) + '</td><td class="num">' + (m.typ === 'F11' ? fmtM2(p.gbf_a) + ' / ' + fmtM2(p.gbf_p) : fmtM2(c.bezugsflaeche)) + '</td>' + (c.bonus ? '<td class="num">' + fmtF(c.bonus) + '</td>' : '') + '<td class="num">' + fmtWP(c.S1) + '</td></tr>' +
'<tr class="sum"><td colspan="' + (c.bonus ? 9 : 8) + '">Summe 1</td><td class="num">' + fmtWP(c.S1) + '</td></tr></tbody></table>';
if (m.typ === 'F5') h += '<div class="small text-muted">Bezug: ' + esc((FK.pik.massnahmen.find(x => x.key === p.pik) || {}).bezug || '') + '</div>';
}
h += '<div class="fw-semibold">Naturhaushalt – Aufwertung der Werte und Funktionen besonderer Bedeutung</div>' + tblFunk(c.fRows, 'FAF', 'Summe 2', k => k.funktion !== 'landschaft' && !BIL.isMinderung(k.afb));
const ab = c.fRows.filter(k => k.funktion !== 'landschaft' && BIL.isMinderung(k.afb));
if (ab.length) h += '<div class="fw-semibold">Naturhaushalt – Abwertung der Werte und Funktionen besonderer Bedeutung</div>' + tblFunk(c.fRows, 'FMF', 'Summe 3', k => k.funktion !== 'landschaft' && BIL.isMinderung(k.afb));
h += '<div class="fw-semibold">Landschaftsbild – Aufwertung der landschaftsästhetischen Funktion</div>' + tblFunk(c.fRows, 'FAF', 'Summe 4', k => k.funktion === 'landschaft');
h += '<table><tbody><tr class="sum"><td>Kompensationsleistung Naturhaushalt (L1) = Summe 1 + Summe 2 − Summe 3</td><td class="num">' + fmtWP(c.L1) + '</td></tr><tr class="sum"><td>Kompensationsleistung landschaftsästhetische Funktion (L2) = Summe 4</td><td class="num">' + fmtWP(c.L2) + '</td></tr></tbody></table>';
if (c.warn.length) h += '<div class="small warn">' + c.warn.map(esc).join('<br>') + '</div>';
});
if (G.mast && f1.land.ersatz) {
const e = f1.land.ersatz;
h += '<h5>F12 Ermittlung der Ersatzzahlung bei mastenartigen Eingriffen über 50 m Höhe</h5><table><thead><tr><th>Bewertungsstufe Landschaftsbild</th><th class="num">ABL %</th><th class="num">BKG €</th><th class="num">FKS % von BKG</th><th class="num">FKS €</th><th class="num">ESZ €</th></tr></thead><tbody>';
e.rows.forEach(r => { h += '<tr><td>' + r.stufe + '</td><td class="num">' + r.abl + '</td><td class="num">' + fmtEur(P.eingriff.landschaft.baukosten) + '</td><td class="num">' + r.proz + '</td><td class="num">' + fmtEur(r.fks) + '</td><td class="num">' + fmtEur(r.esz) + '</td></tr>'; });
h += '<tr class="sum"><td colspan="5">Summe</td><td class="num">' + fmtEur(e.summe) + '</td></tr><tr><td colspan="5">' + esc(e.rep.name) + '</td><td class="num">' + fmtEur(e.korr) + '</td></tr><tr class="sum"><td colspan="5">Ersatzzahlung gesamt (E1)</td><td class="num">' + fmtEur(e.E1) + '</td></tr></tbody></table>';
}
h += '<h5>' + (G.mast ? 'F14 Gesamtbilanz mastenartige Eingriffe > 50 m' : 'F13 Gesamtbilanz') + '</h5><table><thead><tr><th>Bewertungskomponente</th><th class="num">WP</th></tr></thead><tbody>' +
'<tr><td>Kompensationsbedarf Naturhaushalt (B1) aus Formblatt F1</td><td class="num">' + fmtWP(G.B1) + '</td></tr>' +
(G.mast ? '' : '<tr><td>Kompensationsbedarf landschaftsästhetische Funktion (B2) aus Formblatt F1</td><td class="num">' + fmtWP(G.B2) + '</td></tr>') +
'<tr><td>Kompensationsleistung Naturhaushalt (L1) aus den Formblättern F2 bis F11</td><td class="num">' + fmtWP(G.L1) + '</td></tr>' +
(G.mast ? '' : '<tr><td>Kompensationsleistung landschaftsästhetische Funktion (L2) aus den Formblättern F2 bis F11</td><td class="num">' + fmtWP(G.L2) + '</td></tr>') +
'<tr class="sum"><td>(Summe B1 − Summe L1) Bilanzwert Naturhaushalt</td><td class="num ' + (G.bil1 <= 0 ? 'ok' : 'warn') + '">' + fmtWP(G.bil1) + '</td></tr>' +
(G.mast ? '<tr class="sum"><td>Ersatzzahlung gesamt (E1) aus Formblatt F12</td><td class="num">' + fmtEur(G.E1) + '</td></tr>' : '<tr class="sum"><td>(Summe B2 − Summe L2) Bilanzwert landschaftsästhetische Funktion</td><td class="num ' + (G.bil2 <= 0 ? 'ok' : 'warn') + '">' + fmtWP(G.bil2) + '</td></tr>') +
'</tbody></table><div class="small text-muted">Bei einem Wert ≤ 0 ist der Eingriff vollständig kompensiert. Abkürzungen nach Anlage 7: BTC/BTB Biotoptypencode/-bezeichnung, GBF Größe der betroffenen Fläche, BW Biotopwert, PW Planungswert, EWB Einwirkungsbereich, BSF Bewertungsstufe der Funktion, AFB Art der Funktionsbetroffenheit, FMF/FAF Funktionsminderungs-/-aufwertungsfaktor, BFB/BFF Bewertung Fläche nach Biotoptypen/Funktionen.</div>';
$('#box-form').html(h);
}
/* ---------------------------------------------------- Global-UI */
function renderAll() { renderMeta(); renderEF(); renderFK(); renderLand(); renderM(); refreshCalc(); }
$('#btn-neu').on('click', () => { if (!confirm('Alle Eingaben verwerfen und neues Vorhaben beginnen?')) return; KARTE.clear(); P = neuesProjekt(); renderAll(); save(); showTab('tab-vorhaben'); });
$('#btn-export').on('click', () => { download(dateiname('_bilanz.json'), JSON.stringify(snapshot(), null, 1)); });
$('#btn-import').on('click', () => $('#file-import').trigger('click'));
$('#file-import').on('change', function () {
const file = this.files[0]; if (!file) return;
const fr = new FileReader();
fr.onload = () => { try { const o = JSON.parse(fr.result); if (!o.eingriff) throw new Error('kein Bilanz-Projekt'); loadProjekt(o); save(); toast('Vorhaben geladen.', 'success'); } catch (e) { toast('Datei konnte nicht gelesen werden: ' + e.message, 'danger'); } };
fr.readAsText(file); this.value = '';
});
$('#btn-geojson').on('click', () => {
const gj = KARTE.toGeoJSON(); const G = BIL.calcGesamt(P);
gj.features.forEach(ft => {
const ref = (ft.properties.ref || '').split(':'); const id = ref[1];
if (ref[0] === 'ef') { const b = G.f1.bestand.find(x => x.id == id), p = G.f1.planung.find(x => x.id == id); if (b) Object.assign(ft.properties, { bez: b.bez, bt_bestand: b.btc, bw: b.wert, bt_planung: p.btc, pw: p.wert, gbf: b.gbf, wp_bestand: BIL.r2(b.wp), wp_planung: BIL.r2(p.wp) }); }
if (ref[0] === 'm') { const x = G.ms.find(y => y.m.id == id); if (x) Object.assign(ft.properties, { name: x.m.name, typ: x.m.typ, bt_ausgang: x.m.bt_ausgang, bw: x.c.bw, bt_ziel: x.m.bt_ziel, pw: x.c.pw, gbf: x.c.g, L1: BIL.r2(x.c.L1), L2: BIL.r2(x.c.L2) }); }
if (ref[0] === 'fk') { const k = G.f1.fkRows.find(x => x.id == id); if (k) Object.assign(ft.properties, { funktion: funkName(k.funktion), bsf: k.bsf, afb: k.afb, faktor: k.faktor, gbf: k.gbf, wp: BIL.r2(k.wp) }); }
});
download(dateiname('_flaechen_epsg25833.geojson'), JSON.stringify(gj), 'application/geo+json');
});
$('#btn-print').on('click', () => { renderForm(); showTab('tab-form'); setTimeout(() => window.print(), 300); });
$('#btn-hilfe').on('click', () => bootstrap.Modal.getOrCreateInstance('#modal-hilfe').show());
$('#btn-form-refresh').on('click', () => renderForm());
$('button[data-bs-target="#tab-form"]').on('shown.bs.tab', () => renderForm());
/* ------------------------------------------------------- Start */
$.when($.getJSON('data/biotopwertliste.json'), $.getJSON('data/faktoren.json')).done((a, b) => {
BWL = a[0]; FK = b[0];
BIL.init(BWL, FK);
FUNK_ALL = FK.funktionen.concat([{ key: 'landschaft', name: 'Landschaftsästhetische Funktion', schutzgut: 'Landschaftsbild', ewb: ['UE', 'EE'] }]);
if ($.fn.selectpicker && $.fn.selectpicker.Constructor) $.fn.selectpicker.Constructor.BootstrapVersion = '5';
buildOptions();
$('#sel-mtyp').html(optList(FK.massnahmentypen, 'F2', t => t.key, t => t.key + ' – ' + t.name));
KARTE.init('map', { onDrawEnd, onSelect, onModify, onDeleteGeometry, onCoord: t => $('#map-coord').text(t), onDrawState: on => { $('#btn-draw-cancel').prop('hidden', !on); if (!on && !drawCtx) $('#map-hint').text('Auswählen: Klick auf eine Fläche wählt den Eintrag.'); } });
let stored = null;
try { stored = JSON.parse(localStorage.getItem(LS_KEY) || 'null'); } catch (e) { }
if (stored) loadProjekt(stored); else { P = neuesProjekt(); renderAll(); }
$('#autosave-info').text(stored ? 'Stand aus Zwischenspeicher geladen' : 'Biotopwertliste: ' + BWL.typen.filter(t => t.ebene === 'blatt').length + ' Biotoptypen');
}).fail((x, s, e) => { alert('Daten konnten nicht geladen werden (' + s + '). Die Seite muss über einen Webserver (http://) aufgerufen werden.'); });
})();
+262
View File
@@ -0,0 +1,262 @@
/* 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; }
};
})();
+217
View File
@@ -0,0 +1,217 @@
/* karte.js – OpenLayers-Karte (EPSG:25833) mit GeoSN-Hintergrunddiensten,
* Zeichen-/Bearbeitungswerkzeugen und Flächenberechnung.
*/
window.KARTE = (function () {
proj4.defs('EPSG:25833', '+proj=utm +zone=33 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs');
ol.proj.proj4.register(proj4);
const PROJ = ol.proj.get('EPSG:25833');
PROJ.setExtent([150000, 5300000, 650000, 5950000]);
const SACHSEN = [280000, 5560000, 500000, 5720000];
const COLORS = { eingriff: '#d62728', kompensation: '#2ca02c', funktionsraum: '#ff7f0e', standort: '#9467bd', wirkzone: '#9467bd', baum: '#17becf' };
const hex2rgba = (h, a) => { const n = parseInt(h.slice(1), 16); return `rgba(${(n >> 16) & 255},${(n >> 8) & 255},${n & 255},${a})`; };
let map, view, src, layerVec, srcZonen, layerZonen, bases = {}, layerFlst, draw = null, modify, select, snap, cb = {}, tool = 'select', seq = 1;
function styleFn(feature, resolution) {
const rolle = feature.get('rolle') || 'eingriff';
const c = COLORS[rolle] || '#333';
const sel = feature.get('_sel');
const geomType = feature.getGeometry().getType();
const label = feature.get('label') || '';
if (geomType === 'Point') {
if (rolle === 'baum') {
const dm = +feature.get('kronen_dm') || 8;
const rPx = Math.max(4, dm / 2 / resolution);
return new ol.style.Style({
image: new ol.style.Circle({ radius: rPx, fill: new ol.style.Fill({ color: hex2rgba(c, sel ? .55 : .35) }), stroke: new ol.style.Stroke({ color: sel ? '#0d6efd' : c, width: sel ? 3 : 2 }) }),
text: label ? new ol.style.Text({ text: label, offsetY: -rPx - 8, font: '12px sans-serif', fill: new ol.style.Fill({ color: '#222' }), stroke: new ol.style.Stroke({ color: '#fff', width: 3 }) }) : undefined
});
}
return new ol.style.Style({
image: new ol.style.RegularShape({ points: 4, radius: 9, angle: Math.PI / 4, fill: new ol.style.Fill({ color: hex2rgba(c, .8) }), stroke: new ol.style.Stroke({ color: sel ? '#0d6efd' : '#fff', width: 2 }) }),
text: label ? new ol.style.Text({ text: label, offsetY: -16, font: 'bold 12px sans-serif', fill: new ol.style.Fill({ color: '#222' }), stroke: new ol.style.Stroke({ color: '#fff', width: 3 }) }) : undefined
});
}
return new ol.style.Style({
fill: new ol.style.Fill({ color: hex2rgba(c, sel ? .4 : .22) }),
stroke: new ol.style.Stroke({ color: sel ? '#0d6efd' : c, width: sel ? 3 : 2, lineDash: rolle === 'funktionsraum' ? [8, 6] : undefined }),
text: label ? new ol.style.Text({ text: label, font: '12px sans-serif', overflow: true, fill: new ol.style.Fill({ color: '#222' }), stroke: new ol.style.Stroke({ color: '#fff', width: 3 }) }) : undefined
});
}
const zonenStyle = new ol.style.Style({
fill: new ol.style.Fill({ color: 'rgba(148,103,189,.10)' }),
stroke: new ol.style.Stroke({ color: '#9467bd', width: 1.5, lineDash: [4, 6] })
});
function wms(url, layers, fmt) {
return new ol.source.TileWMS({
url, params: { LAYERS: layers, FORMAT: fmt || 'image/png', TILED: true, TRANSPARENT: fmt !== 'image/jpeg' },
projection: 'EPSG:25833', crossOrigin: 'anonymous',
attributions: '© GeoSN (<a href="https://www.govdata.de/dl-de/by-2-0" target="_blank" rel="noopener">dl-de/by-2-0</a>)'
});
}
function init(target, callbacks) {
cb = callbacks || {};
bases.osm = new ol.layer.Tile({ source: new ol.source.OSM(), visible: true });
bases.dop = new ol.layer.Tile({ source: wms('https://geodienste.sachsen.de/wms_geosn_dop-rgb/guest', 'sn_dop_020', 'image/jpeg'), visible: false });
bases.webatlas = new ol.layer.Tile({ source: wms('https://geodienste.sachsen.de/wms_geosn_webatlas-sn/guest', 'Vegetation,Siedlung,Gewaesser,Verkehr,Administrative_Einheiten,Beschriftung', 'image/png'), visible: false });
layerFlst = new ol.layer.Tile({ source: wms('https://geodienste.sachsen.de/wms_geosn_alkis-adv/guest', 'adv_alkis_flurstuecke', 'image/png'), visible: false, minZoom: 9, opacity: .9 });
srcZonen = new ol.source.Vector();
layerZonen = new ol.layer.Vector({ source: srcZonen, style: zonenStyle });
src = new ol.source.Vector();
layerVec = new ol.layer.Vector({ source: src, style: styleFn });
view = new ol.View({ projection: PROJ, center: [390000, 5640000], zoom: 3, minZoom: 1, maxZoom: 14, constrainResolution: false });
map = new ol.Map({
target,
layers: [bases.osm, bases.dop, bases.webatlas, layerFlst, layerZonen, layerVec],
view,
controls: ol.control.defaults.defaults({ attributionOptions: { collapsible: true } }).extend([new ol.control.ScaleLine({ bar: false })])
});
const el = document.getElementById(target);
const fitInitial = () => { const s = map.getSize(); if (s && s[0] > 0 && s[1] > 0) { view.fit(SACHSEN, { padding: [20, 20, 20, 20] }); return true; } return false; };
if (!fitInitial()) {
const ro1 = new ResizeObserver(() => { map.updateSize(); if (fitInitial()) ro1.disconnect(); });
ro1.observe(el);
}
new ResizeObserver(() => map.updateSize()).observe(el);
// Fallback: ResizeObserver feuert in verborgenen Tabs nicht → Timer und visibilitychange
let tries = 0;
const iv = setInterval(() => { map.updateSize(); if (fitInitial() || ++tries > 60) clearInterval(iv); }, 300);
document.addEventListener('visibilitychange', () => { map.updateSize(); });
window.addEventListener('resize', () => map.updateSize());
select = new ol.interaction.Select({ layers: [layerVec], style: null, hitTolerance: 4 });
select.on('select', e => {
e.deselected.forEach(f => f.set('_sel', false));
e.selected.forEach(f => f.set('_sel', true));
const f = e.selected[0];
if (tool === 'delete' && f) { cb.onDeleteGeometry && cb.onDeleteGeometry(f.get('fid')); select.getFeatures().clear(); f.set('_sel', false); return; }
cb.onSelect && cb.onSelect(f ? f.get('fid') : null);
});
map.addInteraction(select);
modify = new ol.interaction.Modify({ features: select.getFeatures() });
modify.setActive(false);
modify.on('modifyend', e => { const ids = []; e.features.forEach(f => ids.push(f.get('fid'))); cb.onModify && cb.onModify(ids); });
map.addInteraction(modify);
snap = new ol.interaction.Snap({ source: src });
map.addInteraction(snap);
map.on('pointermove', e => {
if (cb.onCoord) { const c = e.coordinate; cb.onCoord('E ' + Math.round(c[0]) + ' N ' + Math.round(c[1]) + ' (UTM 33)'); }
});
document.addEventListener('keydown', e => { if (e.key === 'Escape' && draw) cancelDraw(); });
return map;
}
function setBase(key) { Object.keys(bases).forEach(k => bases[k].setVisible(k === key)); }
function setFlst(on) { layerFlst.setVisible(!!on); }
function setTool(t) {
tool = t;
modify.setActive(t === 'modify');
select.setActive(true);
}
function newFid() { let id; do { id = 'f' + (seq++); } while (src.getFeatureById(id)); return id; }
/** Zeichnen starten. rolle: eingriff|kompensation|funktionsraum|standort|baum; type: Polygon|Point */
function startDraw(rolle, type, ctx) {
cancelDraw();
select.setActive(false);
draw = new ol.interaction.Draw({ source: src, type: type || 'Polygon', stopClick: true });
draw.on('drawend', e => {
const f = e.feature;
const fid = newFid();
f.setId(fid);
f.setProperties({ fid, rolle });
setTimeout(() => { cancelDraw(); cb.onDrawEnd && cb.onDrawEnd(f, rolle, ctx); }, 0);
});
map.addInteraction(draw);
document.getElementById(map.getTargetElement().id).classList.add('drawing');
cb.onDrawState && cb.onDrawState(true, rolle, type);
}
function cancelDraw() {
if (draw) { map.removeInteraction(draw); draw = null; }
select.setActive(true);
map.getTargetElement().classList.remove('drawing');
cb.onDrawState && cb.onDrawState(false);
}
const isDrawing = () => !!draw;
function getFeature(fid) { return fid ? src.getFeatureById(fid) : null; }
function area(fid) {
const f = getFeature(fid); if (!f) return null;
const g = f.getGeometry();
if (g.getType() === 'Point') {
const dm = +f.get('kronen_dm') || 0; return Math.PI * (dm / 2) * (dm / 2);
}
return ol.sphere.getArea(g, { projection: 'EPSG:25833' });
}
function setProps(fid, props) { const f = getFeature(fid); if (f) f.setProperties(props); }
function removeFeature(fid) {
const f = getFeature(fid); if (!f) return;
select.getFeatures().remove(f); src.removeFeature(f);
}
function zoomTo(fid) {
const f = getFeature(fid); if (!f) return;
const g = f.getGeometry();
if (g.getType() === 'Point') view.animate({ center: g.getCoordinates(), zoom: Math.max(view.getZoom(), 10) });
else view.fit(g.getExtent(), { padding: [40, 40, 40, 40], maxZoom: 12, duration: 300 });
}
function selectFeature(fid) {
select.getFeatures().forEach(f => f.set('_sel', false));
select.getFeatures().clear();
const f = getFeature(fid);
if (f) { select.getFeatures().push(f); f.set('_sel', true); }
}
function refresh() { src.changed(); }
/** Wirkzonen-Kreise um Standort (fid) mit Radien in m */
function setZonen(fid, r1, r2) {
srcZonen.clear();
const f = getFeature(fid); if (!f || f.getGeometry().getType() !== 'Point') return;
const c = f.getGeometry().getCoordinates();
[r1, r2].filter(r => r > 0).forEach(r => srcZonen.addFeature(new ol.Feature(ol.geom.Polygon.fromCircle(new ol.geom.Circle(c, r), 96))));
}
function clearZonen() { srcZonen.clear(); }
const fmt = () => new ol.format.GeoJSON({ dataProjection: 'EPSG:25833', featureProjection: 'EPSG:25833' });
function toGeoJSON() {
const feats = src.getFeatures().map(f => { const c = f.clone(); c.setId(f.getId()); c.unset('_sel'); return c; });
const o = fmt().writeFeaturesObject(feats);
o.crs = { type: 'name', properties: { name: 'urn:ogc:def:crs:EPSG::25833' } };
return o;
}
function toGeoJSONWgs84() {
const feats = src.getFeatures().map(f => { const c = f.clone(); c.setId(f.getId()); c.unset('_sel'); return c; });
return new ol.format.GeoJSON({ dataProjection: 'EPSG:4326', featureProjection: 'EPSG:25833' }).writeFeaturesObject(feats, { decimals: 7 });
}
function loadGeoJSON(obj) {
src.clear(); srcZonen.clear(); seq = 1;
if (!obj || !obj.features) return;
const feats = fmt().readFeatures(obj);
feats.forEach(f => {
const fid = f.get('fid') || f.getId() || newFid();
f.setId(fid); f.set('fid', fid); f.set('_sel', false);
const n = parseInt(String(fid).replace(/^f/, ''), 10); if (isFinite(n) && n >= seq) seq = n + 1;
});
src.addFeatures(feats);
}
function clear() { src.clear(); srcZonen.clear(); seq = 1; }
function fitAll() {
if (src.getFeatures().length) view.fit(src.getExtent(), { padding: [40, 40, 40, 40], maxZoom: 12 });
}
function centerLonLat(lon, lat, zoom) {
const c = ol.proj.fromLonLat([lon, lat], PROJ);
if (document.hidden) { view.setCenter(c); view.setZoom(zoom || 11); }
else view.animate({ center: c, zoom: zoom || 11, duration: 400 });
}
/** Ortssuche über OSM Nominatim */
function search(q) {
return $.getJSON('https://nominatim.openstreetmap.org/search', { format: 'json', q, countrycodes: 'de', limit: 5, 'accept-language': 'de' });
}
return { init, setBase, setFlst, setTool, startDraw, cancelDraw, isDrawing, getFeature, area, setProps, removeFeature, zoomTo, selectFeature, refresh, setZonen, clearZonen, toGeoJSON, toGeoJSONWgs84, loadGeoJSON, clear, fitAll, centerLonLat, search, get map() { return map; }, COLORS };
})();
+119
View File
@@ -0,0 +1,119 @@
import pdfplumber, re, json
PDF='he.pdf'
COLS=['btc','btb','schutz','lrt','rl','btlnk','sbk3','sbk2','bw','kag','pw','aas_b','aas_p']
HG={'01':'Wälder und Forsten','02':'Gebüsche, Hecken und Gehölze','03':'Fließgewässer','04':'Stillgewässer','05':'Moore und Sümpfe','06':'Grünland','07':'Staudenfluren und Säume','08':'Heiden und Magerrasen','09':'Fels-, Gesteins- und Rohbodenbiotope','10':'Ackerland, Gartenbau und Sonderkulturen','11':'Siedlungsbereiche, Infrastruktur- und Industrieanlagen, sonstige Bebauungsstrukturen und Bauteile'}
STOP={'und','oder','bzw.','bis','sowie','mit','ohne','auf','in','zu','der','des','an'}
def clean(s):
if s is None: return ''
s=s.replace('‐','-').replace('‑','-').replace('­','').replace('\n',' ')
s=re.sub(r'\s+',' ',s).strip()
s=re.sub(r'(?<=[A-Za-zäöüÄÖÜß\)])\d{2}$','',s).strip() # trailing footnote
return s
def dehyph(s):
def rep(m):
return m.group(1) if m.group(2) not in STOP else m.group(0)
return re.sub(r'([a-zäöüß])- (?=([a-zäöüß.]+))',lambda m: m.group(1) if m.group(2) not in STOP else m.group(0), s)
rows=[]
settings={"vertical_strategy":"lines","horizontal_strategy":"lines","snap_tolerance":4,"intersection_tolerance":6}
with pdfplumber.open(PDF) as pdf:
for pno in range(48,75):
page=pdf.pages[pno]
for t in page.find_tables(settings):
grid=t.extract(); colx=None
for i,row in enumerate(t.rows):
texts=[clean(x) for x in grid[i]]
if texts.count('1')==1 and '12' in texts and '2' in texts and '11' in texts:
colx={}
for j,cell in enumerate(row.cells):
if cell and texts[j] in [str(k) for k in range(1,13)]: colx[int(texts[j])]=(cell[0],cell[2])
for i2 in range(i,min(i+5,len(t.rows))):
tx=[clean(x) for x in grid[i2]]
if 'Bestand' in tx and 'Planung' in tx:
for j,cell in enumerate(t.rows[i2].cells):
if cell and tx[j]=='Bestand': colx['12a']=(cell[0],cell[2])
if cell and tx[j]=='Planung': colx['12b']=(cell[0],cell[2])
break
if not colx: continue
ranges=[colx[k] for k in [1,2,3,4,5,6,7,8,9,10,11,'12a','12b']]
def colof(cell):
xc=(cell[0]+cell[2])/2
for k,(a,b) in enumerate(ranges):
if a-1<=xc<=b+1: return k
return None
for i,row in enumerate(t.rows):
texts=[clean(x) for x in grid[i]]
vals=['']*13; top=bot=None
for j,cell in enumerate(row.cells):
if not cell or not texts[j]: continue
k=colof(cell)
if k is None: continue
vals[k]=(vals[k]+' '+texts[j]).strip()
top=cell[1] if top is None else min(top,cell[1]); bot=cell[3] if bot is None else max(bot,cell[3])
if not any(vals): continue
if vals[0] in ('1','BTC') or vals[1] in ('2','BTB') or vals[6] in ('SBK 3 /','WBK') or vals[11] in ('AAS','Bestand'): continue
rows.append(dict(zip(COLS,vals))|{'page':pno+1,'top':top,'bot':bot})
BTC_RE=re.compile(r'^(\d\d\.\d\d\.\d\d\d)(\*{0,2})\s*(\d{2})?$'); HG_RE=re.compile(r'^(\d\d)\.$')
main=[r for r in rows if BTC_RE.match(r['btc'])]
frags=[r for r in rows if not BTC_RE.match(r['btc']) and not HG_RE.match(r['btc'])]
for f in frags:
best=None;bo=-1e9
for r in main:
if r['page']!=f['page']: continue
ov=min(r['bot'],f['bot'])-max(r['top'],f['top'])
if ov>bo: bo=ov;best=r
if best is None or bo< -4:
if any(f[c] for c in COLS): print('ORPHAN',f['page'],{c:f[c] for c in COLS if f[c]})
continue
for c in COLS:
if f[c]:
# order by vertical position
if f['top']<best['top']: best[c]=(f[c]+' '+best[c]).strip()
else: best[c]=(best[c]+' '+f[c]).strip()
KAG_RE=re.compile(r'^([ABCD][12]?(?:/[ABCD][12]?)*)'); RL_RE=re.compile(r'^(k\.?A\.|[0-3RVx*](?:[/-][0-3])?)')
def num(s,btc,anom):
s=s.replace(' ','')
if s in ('–','','-'): return None,None
if re.fullmatch(r'\d{1,2}',s): return int(s),None
if re.fullmatch(r'\d{1,2}/(\d{1,2}|–)',s):
a,b=s.split('/'); return int(a),(int(b) if b!='–' else None)
anom.append(('num',btc,s)); return None,None
typen=[]; anom=[]
for r in main:
m=BTC_RE.match(r['btc']); btc=m.group(1)
d={'btc':btc,'hg':btc[:2],'stern':len(m.group(2))}
d['name']=re.sub(r'\)\s+\d{2}$',')',dehyph(r['btb'])).strip()
d['schutz']=r['schutz'] if r['schutz'] in ('§','(§)','((§))') else '–'
if r['schutz'] not in ('§','(§)','((§))','–','','-','--'): anom.append(('schutz',btc,r['schutz']))
lrt=r['lrt'].replace('( ','(').replace(' )',')'); lrt=re.sub(r'(?<=\))\s*\d{2}$','',lrt).strip()
d['lrt']=lrt if lrt not in ('','–','-') else '–'
rl=r['rl'].replace(' ',''); mm=RL_RE.match(rl)
d['rl']=mm.group(1).replace('k.A.','k. A.') if mm else ('–' if rl in ('–','','-') else rl)
if not mm and rl not in ('–','','-'): anom.append(('rl',btc,rl))
d['bw'],d['bw_alt']=num(r['bw'],btc,anom); d['pw'],d['pw_alt']=num(r['pw'],btc,anom)
k=r['kag'].replace(' ',''); k=re.sub(r'(?<=\d)\d{2}(?=/|$)','',k); mk=KAG_RE.match(k)
d['kag']=mk.group(1) if mk else None
if not mk and k not in ('–','','-'): anom.append(('kag',btc,k))
def aas(s):
s=s.replace(' ','')
if s in ('–','','-'): return []
if re.fullmatch(r'\d{1,2}(,\d{1,2})*',s): return [int(x) for x in s.split(',')]
anom.append(('aas',btc,s)); return []
d['aas_b']=aas(r['aas_b']); d['aas_p']=aas(r['aas_p'])
d['ebene']='blatt' if (d['bw'] is not None or d['pw'] is not None) else ('gruppe' if btc.endswith('000') else 'typ')
d['btlnk']=r['btlnk'].replace('–','').strip(); d['sbk']=(r['sbk3']+' / '+r['sbk2']).replace('–','').strip(' /')
typen.append(d)
defs=json.load(open('bwl_defs.json'))
for d in typen:
if d['btc'] in defs: d['definition']=defs[d['btc']]['definition']
print(len(typen),'typen;', sum(1 for d in typen if d['ebene']=='blatt'),'blatt;', sum(1 for d in typen if 'definition' in d),'mit Definition')
print('ANOM',anom)
for d in typen:
if d['bw'] is None and d['ebene']=='blatt': print('nur PW:',d['btc'],d['name'],d['pw'])
if re.search(r'\d$',d['name']) or ' ' in d['name'] or not d['name']: print('CHECK',d['btc'],repr(d['name']))
if re.search(r'[a-zäöü]- [a-zäöü]',d['name']): print('HYPH',d['btc'],d['name'])
if d['ebene']=='blatt' and d['bw'] is not None and d['kag'] is None and d['pw'] is not None: print('KAG?',d['btc'],d['name'],d['bw'],d['pw'])
out={'quelle':'Biotopwertliste Sachsen – Anlage 2 der „Grundlagen für die Anlagen zur Sächsischen Kompensationsverordnung“ (TU Dresden, Prof. Schmidt / Froelich & Sporbeck), Stand 01/2017; entnommen aus der kombinierten PDF des Landratsamtes Mittelsachsen (Handlungsempfehlung 2009 + Überarbeitung 2017)','stand':'01/2017','hinweis':'Automatisch aus der PDF extrahiert (pdfplumber). BTLNK/SBK-Spalten nur informativ. Fußnoten der Quelle (KAG altersabhängig, Natürlichkeit) sind in kag/pw_alt nur teilweise abgebildet.','hauptgruppen':[{'nr':k,'name':v} for k,v in HG.items()],'typen':typen}
json.dump(out,open('biotopwertliste.json','w'),ensure_ascii=False,indent=1)
print('written', len(json.dumps(out,ensure_ascii=False)),'bytes')
for b in ['02.02.470','02.02.500','11.02.500','11.06.140','11.06.200','10.03.100','02.02.520','01.12.200','03.01.150','05.02.000']:
d=[x for x in typen if x['btc']==b][0]; print(b,'|',d['name'],'|',d['bw'],d['bw_alt'],d['kag'],d['pw'],d['pw_alt'],d['aas_b'],d['aas_p'],d['ebene'])