""" sn_plan41/ui/tab_b_logic.py – Fachlogik für Tab B (Druck) """ from __future__ import annotations import math from typing import Any from sn_basis.functions.qt_wrapper import QVariant from sn_basis.functions.variable_wrapper import set_variable, get_variable from sn_basis.functions.qgiscore_wrapper import get_layer_extent from sn_basis.functions.qgiscore_wrapper import ( QgsProject, QgsVectorLayer, QgsGeometry, QgsFeature, QgsField, QgsVectorFileWriter, ) from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.layerpruefer import Layerpruefer from sn_plan41.ui.layout import Layout KARTENNAME_VAR = "sn_kartenname" PLOTMASSSTAB_VAR = "sn_plotmassstab" VIEW_VAR = "sn_view" ZIELGROESSE_VAR = "sn_zielgroesse" FORMFAKTOR_VAR = "sn_formfaktor" KARTENNAME_38 = "§38" KARTENNAME_41 = "§41" MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster" THEMA_WIE_KARTENFENSTER = "wie kartenfenster" KARTENNAME_BY_AUSWAHL = { KARTENNAME_38: "Planungsübersicht §38 FlurbG", KARTENNAME_41: "Karte zum Plan über die gemeinschaftlichen und öffentlichen Anlagen (§ 41 FlurbG)", } PLOTMASSSTAB_BY_AUSWAHL = { "1:5.000": "5000", "1:10.000": "10000", "1:15.000": "15000", "1:20.000": "20000", "1:25.000": "25000", "1:50.000": "50000", "1:100.000": "100000", } # Breite x Höhe in mm (Hochformat, DIN-Standard) DIN_GROESSEN: dict[str, tuple[int, int]] = { "DIN A0": (841, 1189), "DIN A1": (594, 841), "DIN A2": (420, 594), "DIN A3": (297, 420), "DIN A4": (210, 297), } DIN_STANDARD = "DIN A0" class TabBLogic: """ Kapselt die Fachlogik von Tab B. """ def __init__(self, pruefmanager: Pruefmanager) -> None: self.pruefmanager = pruefmanager @staticmethod def _wkt_rect(x_min: float, y_min: float, x_max: float, y_max: float) -> str: return ( f"POLYGON(({x_min} {y_min}, {x_max} {y_min}, {x_max} {y_max}, " f"{x_min} {y_max}, {x_min} {y_min}))" ) @staticmethod def _set_topological_editing_enabled() -> None: project = QgsProject.instance() set_topological = getattr(project, "setTopologicalEditing", None) if callable(set_topological): try: set_topological(True) except Exception: pass @staticmethod def _apply_atlas_style(layer: Any) -> None: style_path = join_path(get_plugin_root(), "sn_plan41", "assets", "atlasobjekte.qml") if not file_exists(style_path): return try: ok, _ = layer.loadNamedStyle(str(style_path)) if ok: getattr(layer, "triggerRepaint", lambda: None)() except Exception: pass def _create_atlasobjekte_layer( self, layer: Any, extent: Any, pages_x: int, pages_y: int, seite_karte_w: float, seite_karte_h: float, massstab_zahl: float, ) -> Any | None: layer_crs = layer.crs() if hasattr(layer, "crs") else None crs_authid = layer_crs.authid() if layer_crs is not None and hasattr(layer_crs, "authid") else "EPSG:25832" atlas_layer = QgsVectorLayer(f"Polygon?crs={crs_authid}", "Atlasobjekte", "memory") if not atlas_layer or not atlas_layer.isValid(): return None provider = atlas_layer.dataProvider() provider.addAttributes([ QgsField("Seitenzahl", QVariant.Int), ]) atlas_layer.updateFields() tile_w_m = seite_karte_w * massstab_zahl / 1000.0 tile_h_m = seite_karte_h * massstab_zahl / 1000.0 x_min = extent.xMinimum() y_min = extent.yMinimum() x_max = extent.xMaximum() y_max = extent.yMaximum() seitenzahl = 1 features = [] for row_idx in range(pages_y): tile_y_max = y_max - row_idx * tile_h_m tile_y_min = tile_y_max - tile_h_m for col_idx in range(pages_x): tile_x_min = x_min + col_idx * tile_w_m tile_x_max = tile_x_min + tile_w_m tile_geom = QgsGeometry.fromWkt( self._wkt_rect(tile_x_min, tile_y_min, tile_x_max, tile_y_max) ) feat = QgsFeature(atlas_layer.fields()) feat.setGeometry(tile_geom) feat["Seitenzahl"] = seitenzahl features.append(feat) seitenzahl += 1 if not features: return None provider.addFeatures(features) atlas_layer.updateExtents() self._set_topological_editing_enabled() verfahrens_db = get_variable("verfahrens_db", scope="project") or "" print(f"[TabBLogic] Atlasobjekte: verfahrens_db='{verfahrens_db}'") if not verfahrens_db: QgsProject.instance().addMapLayer(atlas_layer) self._apply_atlas_style(atlas_layer) print("[TabBLogic] Atlasobjekte temporär ins Projekt geladen") return atlas_layer opts = QgsVectorFileWriter.SaveVectorOptions() opts.driverName = "GPKG" opts.fileEncoding = "UTF-8" opts.layerName = "Atlasobjekte" if file_exists(verfahrens_db): opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer else: opts.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile err_result = QgsVectorFileWriter.writeAsVectorFormatV3( atlas_layer, verfahrens_db, QgsProject.instance().transformContext(), opts, ) # QGIS-Versionen liefern hier entweder nur WriterError # oder ein Tupel (WriterError, msg, newPath, layerName). err_code = err_result[0] if isinstance(err_result, tuple) else err_result if err_code != QgsVectorFileWriter.NoError: print(f"[TabBLogic] Atlasobjekte schreiben fehlgeschlagen: err={err_result}") return None for existing in QgsProject.instance().mapLayersByName("Atlasobjekte"): try: QgsProject.instance().removeMapLayer(existing.id()) except Exception: pass uri = f"{verfahrens_db}|layername=Atlasobjekte" loaded_layer = QgsVectorLayer(uri, "Atlasobjekte", "ogr") if not loaded_layer or not loaded_layer.isValid(): print(f"[TabBLogic] Atlasobjekte laden aus GPKG fehlgeschlagen: uri='{uri}'") return None QgsProject.instance().addMapLayer(loaded_layer) self._apply_atlas_style(loaded_layer) print("[TabBLogic] Atlasobjekte aus Verfahrens-DB geladen und gestylt") return loaded_layer def set_kartenname_for_auswahl(self, auswahl: str) -> None: """Setzt die Projektvariable ``sn_kartenname`` anhand der Kartennamen-Auswahl.""" kartenname = KARTENNAME_BY_AUSWAHL.get(auswahl, "") set_variable(KARTENNAME_VAR, kartenname, scope="project") def set_plotmassstab_for_auswahl(self, auswahl: str, aktueller_massstab: float | None = None) -> None: """Setzt die Projektvariable ``sn_plotmassstab`` anhand der Maßstabsauswahl.""" if auswahl == MASSSTAB_WIE_KARTENFENSTER: if aktueller_massstab and aktueller_massstab > 0: set_variable(PLOTMASSSTAB_VAR, str(int(round(aktueller_massstab))), scope="project") else: set_variable(PLOTMASSSTAB_VAR, "", scope="project") return value = PLOTMASSSTAB_BY_AUSWAHL.get(auswahl, "") set_variable(PLOTMASSSTAB_VAR, value, scope="project") def set_view_for_auswahl(self, auswahl: str) -> None: """Setzt ``sn_view`` auf ``aktuell`` oder den Namen des gewählten Layerthemas.""" if auswahl == THEMA_WIE_KARTENFENSTER: set_variable(VIEW_VAR, "aktuell", scope="project") return set_variable(VIEW_VAR, auswahl or "", scope="project") def set_zielgroesse_for_auswahl(self, auswahl: str) -> None: """Setzt ``sn_zielgroesse`` auf den gewählten DIN-Namen.""" set_variable(ZIELGROESSE_VAR, auswahl if auswahl in DIN_GROESSEN else DIN_STANDARD, scope="project") def set_formfaktor(self, endlosrolle: bool) -> None: """Setzt ``sn_formfaktor`` auf ``Endlosrolle`` oder ``Blatt``.""" set_variable(FORMFAKTOR_VAR, "Endlosrolle" if endlosrolle else "Blatt", scope="project") # ───────────────────────────────────────────────────────────────────────── # Pipeline: Druckvorlage_anlegen # ───────────────────────────────────────────────────────────────────────── def druckvorlage_anlegen( self, layer: object, kartenname_auswahl: str, massstab_auswahl: str, zielgroesse: str, formfaktor: bool, ) -> dict: """Pipeline 'Druckvorlage_anlegen'. Prüft Parameter, berechnet Plotgröße und stellt bei Bedarf Atlas-Rückfrage. Parameters ---------- layer: Aktuell gewählter Verfahrensgebiet-Layer aus Tab A. Returns ------- dict ``ok`` (bool): Ob die Pipeline erfolgreich durchlaufen werden soll. ``switch_to_tab_a`` (bool): Ob Tab A aktiviert werden soll. ``atlas_seiten`` (int): Anzahl benötigter Seiten (1 = kein Atlas). """ # ─── 1. Verfahrensgebiet-Layer prüfen ───────────────────────────── lp = Layerpruefer(layer=layer) ergebnis = lp.pruefe() if not ergebnis.ok: self.pruefmanager.zeige_hinweis( "Verfahrensgebiets-Layer angeben", "Verfahrensgebiets-Layer angeben", ) return {"ok": False, "switch_to_tab_a": True, "atlas_seiten": 0} layer_id = getattr(layer, "id", lambda: "")() or "" set_variable("sn_verfahrensgebietslayer", layer_id, scope="project") # ─── 2. Kartenname prüfen ───────────────────────────────────────── if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41): self.pruefmanager.zeige_hinweis( "Kartennamen wählen", "Kartennamen wählen", ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} # ─── 3. Maßstab ermitteln ───────────────────────────────────────── if massstab_auswahl == MASSSTAB_WIE_KARTENFENSTER: massstab_str = get_variable(PLOTMASSSTAB_VAR, scope="project") or "" try: massstab_zahl = float(massstab_str) except (ValueError, TypeError): massstab_zahl = 0.0 else: massstab_zahl = float(PLOTMASSSTAB_BY_AUSWAHL.get(massstab_auswahl, 0)) if massstab_zahl <= 0: self.pruefmanager.zeige_hinweis( "Maßstab fehlt", "Kein gültiger Maßstab angegeben.", ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} # ─── 4. Kartenbild berechnen ────────────────────────────────────── # Der Layer wird als metrisch projiziert (Einheit: m) vorausgesetzt, # wie es für deutsche Planungslagen (z.B. EPSG:25832) üblich ist. extent = get_layer_extent(layer) if extent is None: self.pruefmanager.zeige_hinweis( "Fehler", "Layer-Ausdehnung konnte nicht ermittelt werden.", ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} # Naturgröße (m) → Papiergröße (mm): mm = m * 1000 / massstab kartenbild_w = extent.width() * 1000.0 / massstab_zahl kartenbild_h = extent.height() * 1000.0 / massstab_zahl # ─── 5. Plotgröße = Kartenbild + Randabstand (x+210 mm, y+20 mm) ── plotgroesse_w = kartenbild_w + 210.0 plotgroesse_h = kartenbild_h + 20.0 # ─── 6. Zielgröße bestimmen ─────────────────────────────────────── din_dims = DIN_GROESSEN.get(zielgroesse, DIN_GROESSEN[DIN_STANDARD]) if formfaktor: # Endlosrolle: X-Richtung entspricht der Plotgröße ziel_w, ziel_h = plotgroesse_w, float(min(din_dims)) else: ziel_w, ziel_h = float(din_dims[0]), float(din_dims[1]) if ziel_w < DIN_GROESSEN["DIN A4"][0] or ziel_h < DIN_GROESSEN["DIN A4"][1]: self.pruefmanager.zeige_hinweis( "Blattgröße zu klein", "Die Zielgröße darf nicht kleiner als DIN A4 sein.", ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} # ─── 7. Passt auf ein Blatt? -> Layout erzeugen ─────────────────── print(f"[TabBLogic] plotgroesse=({plotgroesse_w:.1f}x{plotgroesse_h:.1f}), " f"zielgroesse=({ziel_w:.1f}x{ziel_h:.1f}), passt={plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h}") if plotgroesse_w > ziel_w or plotgroesse_h > ziel_h: if plotgroesse_w <= ziel_h and plotgroesse_h <= ziel_w: ziel_w, ziel_h = ziel_h, ziel_w if plotgroesse_w <= ziel_w and plotgroesse_h <= ziel_h: kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get( kartenname_auswahl, "Vorlage" ) thema = get_variable(VIEW_VAR, scope="project") or "" print(f"[TabBLogic] frage_text aufrufen, default='{kartenname}', thema='{thema}'") vorlage_name, bestaetigt = self.pruefmanager.frage_text( "Neue Vorlage anlegen", "Bezeichnung der Vorlage:", default_text=kartenname, ) print(f"[TabBLogic] frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}") if not bestaetigt: return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} vorlage_name = (vorlage_name or "").strip() or kartenname print(f"[TabBLogic] Rufe Layout().create_single_page_layout auf: name='{vorlage_name}', " f"page=({ziel_w}x{ziel_h}), map=({kartenbild_w:.1f}x{kartenbild_h:.1f}), " f"massstab={massstab_zahl}, thema='{thema}'") try: Layout().create_single_page_layout( name=vorlage_name, page_width_mm=ziel_w, page_height_mm=ziel_h, map_width_mm=kartenbild_w, map_height_mm=kartenbild_h, extent=extent, plotmassstab=massstab_zahl, thema=thema, ) print("[TabBLogic] create_single_page_layout erfolgreich abgeschlossen") except ValueError as exc: print(f"[TabBLogic] ValueError: {exc}") self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc)) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} except Exception as exc: print(f"[TabBLogic] Exception: {exc!r}") self.pruefmanager.zeige_hinweis("Vorlage anlegen", f"Die Vorlage konnte nicht angelegt werden: {exc}") return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": 1} # ─── 8. Atlas: Anzahl Seiten berechnen ──────────────────────────── # Nutzbarer Kartenbereich pro Atlasseite (abzüglich gleichem Randabstand) seite_karte_w = ziel_w - 210.0 seite_karte_h = ziel_h - 20.0 if seite_karte_w <= 0 or seite_karte_h <= 0: self.pruefmanager.zeige_hinweis( "Blattgröße zu klein", "Die gewählte Zielgröße ist kleiner als der Mindest-Randabstand. " "Bitte eine größere Blattgröße wählen.", ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} pages_x = math.ceil(kartenbild_w / seite_karte_w) pages_y = math.ceil(kartenbild_h / seite_karte_h) anzahl_seiten = pages_x * pages_y ja = self.pruefmanager.frage_ja_nein( "Ausdruck als Atlas anlegen?", f"Für die ausgewählten Parameter sind {anzahl_seiten} Einzelseiten erforderlich.\n" "Ausdruck als Atlas anlegen?", default=True, ) print(f"[TabBLogic] Atlas-Rückfrage: ja={ja}, geplante_seiten={anzahl_seiten}") if not ja: return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten} atlas_layer = self._create_atlasobjekte_layer( layer=layer, extent=extent, pages_x=pages_x, pages_y=pages_y, seite_karte_w=seite_karte_w, seite_karte_h=seite_karte_h, massstab_zahl=massstab_zahl, ) if atlas_layer is None: self.pruefmanager.zeige_hinweis( "Atlasobjekte", "Atlasobjekte-Layer konnte nicht erzeugt werden.", ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} print("[TabBLogic] Atlasobjekte-Layer erfolgreich erzeugt") try: anzahl_seiten = int(atlas_layer.featureCount()) except Exception: pass # Berechne die Kachelgröße aus den Atlasobjekten. # Für die Layout-Kartengröße muss die größte Atlas-Kachel passen, # sonst werden größere Atlasobjekte abgeschnitten. total_w_m = 0.0 total_h_m = 0.0 min_w_m = math.inf min_h_m = math.inf max_w_m = 0.0 max_h_m = 0.0 feature_count = 0 get_features = getattr(atlas_layer, "getFeatures", None) if callable(get_features): try: for feat in get_features(): geom = feat.geometry() if hasattr(feat, "geometry") else None if geom is None or geom.isEmpty(): continue bbox = geom.boundingBox() if hasattr(geom, "boundingBox") else None if bbox is None: continue feat_w_m = float(bbox.width()) feat_h_m = float(bbox.height()) total_w_m += feat_w_m total_h_m += feat_h_m min_w_m = min(min_w_m, feat_w_m) min_h_m = min(min_h_m, feat_h_m) max_w_m = max(max_w_m, feat_w_m) max_h_m = max(max_h_m, feat_h_m) feature_count += 1 except Exception: pass if feature_count > 0: avg_tile_w_m = total_w_m / feature_count avg_tile_h_m = total_h_m / feature_count if not math.isfinite(min_w_m): min_w_m = avg_tile_w_m if not math.isfinite(min_h_m): min_h_m = avg_tile_h_m target_tile_w_m = max_w_m target_tile_h_m = max_h_m else: avg_tile_w_m = seite_karte_w * massstab_zahl / 1000.0 avg_tile_h_m = seite_karte_h * massstab_zahl / 1000.0 min_w_m = avg_tile_w_m min_h_m = avg_tile_h_m max_w_m = avg_tile_w_m max_h_m = avg_tile_h_m target_tile_w_m = avg_tile_w_m target_tile_h_m = avg_tile_h_m # Konvertiere Kachelgrößen zu mm avg_tile_w_mm = avg_tile_w_m * 1000.0 / massstab_zahl avg_tile_h_mm = avg_tile_h_m * 1000.0 / massstab_zahl min_tile_w_mm = min_w_m * 1000.0 / massstab_zahl min_tile_h_mm = min_h_m * 1000.0 / massstab_zahl max_tile_w_mm = max_w_m * 1000.0 / massstab_zahl max_tile_h_mm = max_h_m * 1000.0 / massstab_zahl target_tile_w_mm = target_tile_w_m * 1000.0 / massstab_zahl target_tile_h_mm = target_tile_h_m * 1000.0 / massstab_zahl # Layout-Kartengröße = größte Kachelgröße atlas_map_w = max(1.0, target_tile_w_mm) atlas_map_h = max(1.0, target_tile_h_mm) atlas_page_w = atlas_map_w + 210.0 atlas_page_h = atlas_map_h + 20.0 # Debug: Atlasobjekte Geometrien print( f"[TabBLogic] Atlasobjekte Geometrien (Meter): " f"total_w={total_w_m:.1f}m, total_h={total_h_m:.1f}m, " f"min_w={min_w_m:.1f}m, min_h={min_h_m:.1f}m, " f"avg_w={avg_tile_w_m:.1f}m, avg_h={avg_tile_h_m:.1f}m, " f"max_w={max_w_m:.1f}m, max_h={max_h_m:.1f}m, features={feature_count}" ) print( f"[TabBLogic] Atlas layout Größen: " f"page=(x=10, y=10, w={atlas_page_w:.1f}mm, h={atlas_page_h:.1f}mm), " f"map=(x=10, y=10, w={atlas_map_w:.1f}mm, h={atlas_map_h:.1f}mm), " f"kachel_min_mm=({min_tile_w_mm:.1f}x{min_tile_h_mm:.1f}), " f"kachel_avg_mm=({avg_tile_w_mm:.1f}x{avg_tile_h_mm:.1f}), " f"kachel_max_mm=({max_tile_w_mm:.1f}x{max_tile_h_mm:.1f})" ) kartenname = get_variable(KARTENNAME_VAR, scope="project") or KARTENNAME_BY_AUSWAHL.get( kartenname_auswahl, "Atlas" ) thema = get_variable(VIEW_VAR, scope="project") or "" vorlage_name, bestaetigt = self.pruefmanager.frage_text( "Neue Atlasvorlage anlegen", "Bezeichnung der Vorlage:", default_text=f"{kartenname} Atlas", ) print(f"[TabBLogic] Atlas frage_text Ergebnis: name='{vorlage_name}', bestaetigt={bestaetigt}") if not bestaetigt: return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten} vorlage_name = (vorlage_name or "").strip() or f"{kartenname} Atlas" try: print( f"[TabBLogic] Rufe create_atlas_layout auf: name='{vorlage_name}', " f"page=({atlas_page_w:.1f}x{atlas_page_h:.1f}), " f"map=({atlas_map_w:.1f}x{atlas_map_h:.1f}), massstab={massstab_zahl}, thema='{thema}'" ) Layout().create_atlas_layout( name=vorlage_name, page_width_mm=atlas_page_w, page_height_mm=atlas_page_h, map_width_mm=atlas_map_w, map_height_mm=atlas_map_h, extent=extent, plotmassstab=massstab_zahl, atlas_layer=atlas_layer, thema=thema, ) print("[TabBLogic] create_atlas_layout erfolgreich abgeschlossen") except ValueError as exc: self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", str(exc)) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} except Exception as exc: self.pruefmanager.zeige_hinweis("Atlasvorlage anlegen", f"Die Atlasvorlage konnte nicht angelegt werden: {exc}") return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": anzahl_seiten}