""" Real Estate feature — Geometry utilities. Handles conversion between GeoPolylinie and Shapely polygons, combining parcel geometries, filtering neighbor parcels, fetching parcel polygons from Swisstopo, creating projects with parcel data, and GeoJSON conversion. """ import logging from typing import Optional, Dict, Any, List from shapely.geometry import Polygon from shapely.ops import unary_union from .datamodelFeatureRealEstate import ( Projekt, Parzelle, StatusProzess, GeoPolylinie, GeoPunkt, Kontext, Gemeinde, Kanton, Land, ) from modules.connectors.connectorSwissTopoMapServer import SwissTopoMapServerConnector from .interfaceFeatureRealEstate import getInterface as getRealEstateInterface from modules.datamodels.datamodelUam import User from fastapi import HTTPException, status logger = logging.getLogger(__name__) def geopolylinie_to_shapely_polygon(geopolylinie: GeoPolylinie) -> Polygon: """ Convert GeoPolylinie to Shapely Polygon. Args: geopolylinie: GeoPolylinie instance with punkte list Returns: Shapely Polygon object """ if not geopolylinie or not geopolylinie.punkte: raise ValueError("GeoPolylinie must have at least one point") coordinates = [] for punkt in geopolylinie.punkte: coordinates.append((punkt.x, punkt.y)) if len(coordinates) < 3: raise ValueError("Polygon must have at least 3 points") if coordinates[0] != coordinates[-1]: coordinates.append(coordinates[0]) return Polygon(coordinates) def shapely_polygon_to_geopolylinie(polygon: Polygon) -> GeoPolylinie: """ Convert Shapely Polygon to GeoPolylinie. Args: polygon: Shapely Polygon object Returns: GeoPolylinie instance with LV95 coordinate system """ if not polygon or polygon.is_empty: raise ValueError("Polygon must not be empty") exterior_coords = list(polygon.exterior.coords) if len(exterior_coords) > 1 and exterior_coords[0] == exterior_coords[-1]: exterior_coords = exterior_coords[:-1] punkte = [] for coord in exterior_coords: punkt = GeoPunkt( koordinatensystem="LV95", x=float(coord[0]), y=float(coord[1]), z=None ) punkte.append(punkt) return GeoPolylinie( closed=True, punkte=punkte ) def combine_parcel_geometries(geometries: List[GeoPolylinie]) -> GeoPolylinie: """ Combine multiple parcel geometries into a single outer outline. Uses Shapely union operation to merge polygons and automatically removes internal edges. The result is a clean outer boundary. Args: geometries: List of GeoPolylinie instances to combine Returns: Combined GeoPolylinie representing the outer outline Raises: ValueError: If geometries list is empty or invalid """ if not geometries or len(geometries) == 0: raise ValueError("At least one geometry is required") if len(geometries) == 1: return geometries[0] shapely_polygons = [] for geo in geometries: try: polygon = geopolylinie_to_shapely_polygon(geo) if not polygon.is_empty: shapely_polygons.append(polygon) except Exception as e: logger.warning(f"Error converting geometry to Shapely Polygon: {e}") continue if not shapely_polygons: raise ValueError("No valid geometries to combine") if len(shapely_polygons) == 1: return shapely_polygon_to_geopolylinie(shapely_polygons[0]) try: combined = unary_union(shapely_polygons) if hasattr(combined, 'geoms'): largest = max(combined.geoms, key=lambda p: p.area) combined = largest if combined.is_empty: raise ValueError("Union resulted in empty geometry") result = shapely_polygon_to_geopolylinie(combined) logger.info(f"Combined {len(geometries)} geometries into single outline with {len(result.punkte)} points") return result except Exception as e: logger.error(f"Error combining geometries: {e}", exc_info=True) raise ValueError(f"Failed to combine geometries: {str(e)}") def filter_neighbor_parcels( neighbors: List[Dict[str, Any]], selected_geometries: List[GeoPolylinie] ) -> List[Dict[str, Any]]: """ Filter neighbor parcels to exclude those that are part of the selected parcels. Uses geometric comparison to check if neighbor parcels intersect or touch any of the selected parcel geometries. Args: neighbors: List of neighbor parcel dictionaries (must have 'perimeter' or 'geometry_geojson') selected_geometries: List of GeoPolylinie instances representing selected parcels Returns: Filtered list of neighbor parcels (excluding selected ones) """ if not neighbors or not selected_geometries: return neighbors selected_polygons = [] for geo in selected_geometries: try: polygon = geopolylinie_to_shapely_polygon(geo) if not polygon.is_empty: selected_polygons.append(polygon) except Exception as e: logger.warning(f"Error converting selected geometry for filtering: {e}") continue if not selected_polygons: return neighbors filtered_neighbors = [] for neighbor in neighbors: try: neighbor_geometry = None if neighbor.get("perimeter"): perimeter = neighbor["perimeter"] if isinstance(perimeter, dict) and perimeter.get("punkte"): punkte = [] for p in perimeter["punkte"]: punkt = GeoPunkt( koordinatensystem=p.get("koordinatensystem", "LV95"), x=float(p.get("x", 0)), y=float(p.get("y", 0)), z=p.get("z") ) punkte.append(punkt) neighbor_geometry = GeoPolylinie(closed=True, punkte=punkte) elif neighbor.get("geometry_geojson"): geo_json = neighbor["geometry_geojson"] geometry = geo_json.get("geometry") if isinstance(geo_json, dict) else geo_json if geometry and geometry.get("type") == "Polygon": coordinates = geometry.get("coordinates", []) if coordinates and len(coordinates) > 0: ring = coordinates[0] punkte = [] for coord in ring: if len(coord) >= 2: punkt = GeoPunkt( koordinatensystem="LV95", x=float(coord[0]), y=float(coord[1]), z=float(coord[2]) if len(coord) > 2 else None ) punkte.append(punkt) neighbor_geometry = GeoPolylinie(closed=True, punkte=punkte) if not neighbor_geometry: filtered_neighbors.append(neighbor) continue neighbor_polygon = geopolylinie_to_shapely_polygon(neighbor_geometry) is_selected = False for selected_polygon in selected_polygons: if neighbor_polygon.intersects(selected_polygon) or neighbor_polygon.touches(selected_polygon): area_diff = abs(neighbor_polygon.area - selected_polygon.area) if area_diff < 1.0: is_selected = True break if neighbor_polygon.contains(selected_polygon) or selected_polygon.contains(neighbor_polygon): is_selected = True break if not is_selected: filtered_neighbors.append(neighbor) else: logger.debug(f"Filtered out neighbor parcel {neighbor.get('id')} - part of selected parcels") except Exception as e: logger.warning(f"Error filtering neighbor parcel {neighbor.get('id')}: {e}") filtered_neighbors.append(neighbor) logger.info(f"Filtered {len(neighbors)} neighbors to {len(filtered_neighbors)} (removed {len(neighbors) - len(filtered_neighbors)} selected parcels)") return filtered_neighbors async def fetch_parcel_polygon_from_swisstopo( gemeinde: str, parzellen_nr: str, sr: int = 2056 ) -> Optional[Dict[str, Any]]: """ Holt die vollständige Polygon-Geometrie einer Parzelle von Swisstopo API. Args: gemeinde: Name der Gemeinde (z.B. "Bern") parzellen_nr: Parzellennummer (z.B. "1234") sr: Koordinatensystem (2056=LV95, 4326=WGS84) Returns: Dictionary mit GeoPolylinie-Format für perimeter-Feld, oder None wenn nicht gefunden Format: {"closed": True, "punkte": [{"koordinatensystem": "LV95", "x": ..., "y": ..., "z": None}, ...]} """ try: connector = SwissTopoMapServerConnector() feature = await connector.get_parcel_polygon(gemeinde, parzellen_nr, sr) if not feature: logger.warning(f"Parzelle {gemeinde} {parzellen_nr} nicht gefunden in Swisstopo") return None geometry = feature.get("geometry", {}) if geometry.get("type") == "Polygon": coordinates = geometry.get("coordinates", []) if coordinates and len(coordinates) > 0: ring = coordinates[0] punkte = [] for coord in ring: if len(coord) >= 2: punkt = { "koordinatensystem": "LV95" if sr == 2056 else "WGS84", "x": coord[0], "y": coord[1], "z": coord[2] if len(coord) > 2 else None } punkte.append(punkt) logger.info(f"Successfully fetched polygon with {len(punkte)} points for {gemeinde} {parzellen_nr}") return { "closed": True, "punkte": punkte } logger.warning(f"Unexpected geometry type in Swisstopo response: {geometry.get('type')}") return None except Exception as e: logger.error(f"Error fetching parcel polygon from Swisstopo: {e}", exc_info=True) return None def convert_geojson_to_geopolylinie(geometry_data: Dict[str, Any]) -> Optional[GeoPolylinie]: """Convert GeoJSON geometry to GeoPolylinie format.""" if not geometry_data: return None if "geometry" in geometry_data: geometry_data = geometry_data["geometry"] geometry_type = geometry_data.get("type") coordinates = geometry_data.get("coordinates") if not coordinates or geometry_type != "Polygon": return None if not coordinates or len(coordinates) == 0: return None ring = coordinates[0] punkte = [] for coord in ring: if len(coord) >= 2: punkt = GeoPunkt( koordinatensystem="LV95", x=float(coord[0]), y=float(coord[1]), z=float(coord[2]) if len(coord) > 2 else None ) punkte.append(punkt) if not punkte: return None return GeoPolylinie( closed=True, punkte=punkte ) async def create_project_with_parcel_data( currentUser: User, mandateId: str, projekt_label: str, parzellen_data: List[Dict[str, Any]], status_prozess: Optional[str] = None, ) -> Dict[str, Any]: """ Create a Projekt with one or more Parzellen from provided parcel data. Args: currentUser: Current authenticated user mandateId: Mandate context (from RequestContext / X-Mandate-Id header) projekt_label: Label for the Projekt parzellen_data: List of dictionaries containing parcel information from request status_prozess: Optional project status (defaults to "Eingang") Returns: Dictionary containing created Projekt and list of Parzellen Raises: HTTPException: If Gemeinde or Kanton not found, or validation fails """ try: logger.info(f"Creating project '{projekt_label}' with {len(parzellen_data)} parcel(s) for user {currentUser.id}") realEstateInterface = getRealEstateInterface(currentUser, mandateId=mandateId) if not projekt_label: raise ValueError("Projekt label is required") if not parzellen_data or len(parzellen_data) == 0: raise ValueError("At least one Parzelle data is required") for idx, parzelle_data in enumerate(parzellen_data): if not parzelle_data.get("perimeter"): raise ValueError(f"Parzelle {idx + 1} perimeter is required") # First pass: Collect all parcel geometries for neighbor filtering all_parcel_geometries = [] for parzelle_data in parzellen_data: perimeter = parzelle_data.get("perimeter") if perimeter: if isinstance(perimeter, dict): if "punkte" in perimeter and "closed" in perimeter: try: geo_perimeter = GeoPolylinie(**perimeter) all_parcel_geometries.append(geo_perimeter) except Exception as e: logger.warning(f"Error converting perimeter to GeoPolylinie: {e}") else: converted = convert_geojson_to_geopolylinie(perimeter) if converted: all_parcel_geometries.append(converted) elif isinstance(perimeter, GeoPolylinie): all_parcel_geometries.append(perimeter) created_parzellen = [] parcel_perimeters = [] for idx, parzelle_data in enumerate(parzellen_data): logger.info(f"Processing Parzelle {idx + 1}/{len(parzellen_data)}") parcel_label = parzelle_data.get("id") or parzelle_data.get("number") or parzelle_data.get("label") or "Unknown" existing_parzellen = realEstateInterface.getParzellen( recordFilter={"label": parcel_label, "mandateId": mandateId} ) if existing_parzellen and len(existing_parzellen) > 0: existing_parzelle = existing_parzellen[0] logger.info(f"Parzelle with label '{parcel_label}' already exists (ID: {existing_parzelle.id}), reusing it") if existing_parzelle.perimeter: parcel_perimeters.append(existing_parzelle.perimeter) created_parzellen.append(existing_parzelle) continue logger.info(f"Parzelle with label '{parcel_label}' does not exist, creating new one") gemeinde_id = None canton_abk = parzelle_data.get("canton") municipality_name = parzelle_data.get("municipality_name") logger.debug(f"Resolving Gemeinde/Kanton: canton='{canton_abk}', municipality='{municipality_name}'") if municipality_name and canton_abk: canton_names = { "ZH": "Zürich", "BE": "Bern", "LU": "Luzern", "UR": "Uri", "SZ": "Schwyz", "OW": "Obwalden", "NW": "Nidwalden", "GL": "Glarus", "ZG": "Zug", "FR": "Freiburg", "SO": "Solothurn", "BS": "Basel-Stadt", "BL": "Basel-Landschaft", "SH": "Schaffhausen", "AR": "Appenzell Ausserrhoden", "AI": "Appenzell Innerrhoden", "SG": "St. Gallen", "GR": "Graubünden", "AG": "Aargau", "TG": "Thurgau", "TI": "Tessin", "VD": "Waadt", "VS": "Wallis", "NE": "Neuenburg", "GE": "Genf", "JU": "Jura" } logger.debug("Ensuring Land 'Schweiz' exists") laender = realEstateInterface.getLaender(recordFilter={"label": "Schweiz"}) if not laender: logger.info("Creating Land 'Schweiz'") land = Land( mandateId=mandateId, label="Schweiz", abk="CH" ) land = realEstateInterface.createLand(land) logger.info(f"Created Land 'Schweiz' with ID: {land.id}") else: land = laender[0] logger.debug(f"Found Land 'Schweiz' with ID: {land.id}") logger.debug(f"Looking up Kanton with abk='{canton_abk}'") kantone = realEstateInterface.getKantone(recordFilter={"abk": canton_abk}) logger.debug(f"Found {len(kantone)} Kanton(e) with abk='{canton_abk}'") if not kantone: logger.info(f"Kanton '{canton_abk}' not found, creating it") kanton_label = canton_names.get(canton_abk, canton_abk) kanton = Kanton( mandateId=mandateId, label=kanton_label, abk=canton_abk, id_land=land.id ) kanton = realEstateInterface.createKanton(kanton) logger.info(f"Created Kanton '{kanton_label}' ({canton_abk}) with ID: {kanton.id}") else: kanton = kantone[0] logger.debug(f"Found Kanton: ID={kanton.id}, Label={kanton.label}, abk={kanton.abk}") logger.debug(f"Looking up Gemeinde with label='{municipality_name}' and id_kanton='{kanton.id}'") gemeinden = realEstateInterface.getGemeinden( recordFilter={"label": municipality_name, "id_kanton": kanton.id} ) logger.debug(f"Found {len(gemeinden)} Gemeinde(n) with label='{municipality_name}' and id_kanton='{kanton.id}'") if not gemeinden: logger.info(f"Gemeinde '{municipality_name}' in Kanton '{canton_abk}' not found, creating it") gemeinde = Gemeinde( mandateId=mandateId, label=municipality_name, id_kanton=kanton.id, plz=parzelle_data.get("plz") ) gemeinde = realEstateInterface.createGemeinde(gemeinde) logger.info(f"Created Gemeinde '{municipality_name}' with ID: {gemeinde.id}") else: gemeinde = gemeinden[0] logger.debug(f"Found Gemeinde: ID={gemeinde.id}, Label={gemeinde.label}") gemeinde_id = gemeinde.id logger.info(f"Resolved Gemeinde '{municipality_name}' to ID '{gemeinde_id}'") else: logger.warning(f"Missing Gemeinde/Kanton data: municipality_name={municipality_name}, canton={canton_abk}") alias_tags = [] if parzelle_data.get("egrid"): alias_tags.append(parzelle_data["egrid"]) if parzelle_data.get("number") and parzelle_data["number"] != parzelle_data.get("id"): alias_tags.append(parzelle_data["number"]) strasse_nr = None plz = None address = parzelle_data.get("address") if address: parts = address.split(",") if len(parts) >= 1: strasse_nr = parts[0].strip() plz = parzelle_data.get("plz") logger.debug(f"Parzelle {idx + 1} address data: strasse_nr='{strasse_nr}', plz='{plz}', full_address='{address}'") if not strasse_nr and not plz: logger.warning(f"No address data found for Parzelle {idx + 1} (label: {parcel_label})") kontext_items = [] if parzelle_data.get("egrid"): kontext_items.append(Kontext( thema="EGRID", inhalt=parzelle_data["egrid"] )) if parzelle_data.get("identnd"): kontext_items.append(Kontext( thema="IdentND", inhalt=parzelle_data["identnd"] )) if parzelle_data.get("area_m2"): kontext_items.append(Kontext( thema="Fläche", inhalt=f"{parzelle_data['area_m2']} m²" )) if parzelle_data.get("centroid"): centroid = parzelle_data["centroid"] kontext_items.append(Kontext( thema="Zentrum (LV95)", inhalt=f"X: {centroid.get('x')} m, Y: {centroid.get('y')} m (EPSG:2056)" )) if parzelle_data.get("geoportal_url"): kontext_items.append(Kontext( thema="Geoportal URL", inhalt=parzelle_data["geoportal_url"] )) if parzelle_data.get("municipality_code"): kontext_items.append(Kontext( thema="BFS-Nummer", inhalt=str(parzelle_data["municipality_code"]) )) adjacent_parcel_refs = [] if parzelle_data.get("adjacent_parcels"): neighbors_to_filter = [] for adj_parcel in parzelle_data["adjacent_parcels"]: if isinstance(adj_parcel, dict): neighbors_to_filter.append(adj_parcel) elif isinstance(adj_parcel, str): neighbors_to_filter.append({"id": adj_parcel}) if all_parcel_geometries and neighbors_to_filter: try: filtered_neighbors = filter_neighbor_parcels( neighbors_to_filter, all_parcel_geometries ) for filtered_neighbor in filtered_neighbors: adj_id = filtered_neighbor.get("id") if adj_id: adjacent_parcel_refs.append({"id": adj_id}) except Exception as e: logger.warning(f"Error filtering neighbor parcels: {e}, including all neighbors") for adj_parcel in parzelle_data["adjacent_parcels"]: if isinstance(adj_parcel, dict): adj_id = adj_parcel.get("id") if adj_id: adjacent_parcel_refs.append({"id": adj_id}) elif isinstance(adj_parcel, str): adjacent_parcel_refs.append({"id": adj_parcel}) else: for adj_parcel in parzelle_data["adjacent_parcels"]: if isinstance(adj_parcel, dict): adj_id = adj_parcel.get("id") if adj_id: adjacent_parcel_refs.append({"id": adj_id}) elif isinstance(adj_parcel, str): adjacent_parcel_refs.append({"id": adj_parcel}) perimeter = parzelle_data.get("perimeter") if isinstance(perimeter, dict): if "punkte" in perimeter and "closed" in perimeter: try: perimeter = GeoPolylinie(**perimeter) except Exception as e: raise ValueError(f"Invalid perimeter format: {str(e)}") else: converted = convert_geojson_to_geopolylinie(perimeter) if converted: perimeter = converted else: raise ValueError("Invalid perimeter format: cannot convert to GeoPolylinie") elif isinstance(perimeter, GeoPolylinie): pass else: raise ValueError("Invalid perimeter type: must be dict or GeoPolylinie") baulinie = None geometry = parzelle_data.get("geometry") logger.debug(f"Geometry present: {geometry is not None}") if geometry: logger.debug(f"Geometry type: {type(geometry)}, keys: {list(geometry.keys()) if isinstance(geometry, dict) else 'not a dict'}") baulinie = convert_geojson_to_geopolylinie(geometry) if baulinie: logger.info(f"Extracted baulinie from geometry with {len(baulinie.punkte)} points") else: logger.warning("Failed to extract baulinie from geometry") else: logger.warning("No geometry found in parzelle_data") parzelle_create_data = { "mandateId": mandateId, "label": parcel_label, "parzellenAliasTags": alias_tags, "eigentuemerschaft": None, "strasseNr": strasse_nr, "plz": plz, "perimeter": perimeter, "baulinie": baulinie, "kontextGemeinde": gemeinde_id, "bauzone": None, "az": None, "bz": None, "vollgeschossZahl": None, "anrechenbarDachgeschoss": None, "anrechenbarUntergeschoss": None, "gebaeudehoeheMax": None, "regelnGrenzabstand": [], "regelnMehrlaengenzuschlag": [], "regelnMehrhoehenzuschlag": [], "parzelleBebaut": None, "parzelleErschlossen": None, "parzelleHanglage": None, "laermschutzzone": None, "hochwasserschutzzone": None, "grundwasserschutzzone": None, "parzellenNachbarschaft": adjacent_parcel_refs, "dokumente": [], "kontextInformationen": kontext_items, } logger.debug(f"Creating Parzelle with label: {parzelle_create_data.get('label')}") logger.debug(f"Parzelle mandateId: {parzelle_create_data.get('mandateId')}") logger.debug(f"Parzelle perimeter present: {parzelle_create_data.get('perimeter') is not None}") try: parzelle_instance = Parzelle(**parzelle_create_data) logger.debug(f"Parzelle instance created successfully with ID: {parzelle_instance.id}") except Exception as e: logger.error(f"Error creating Parzelle instance: {str(e)}", exc_info=True) raise try: logger.info(f"Calling createParzelle for Parzelle '{parzelle_instance.label}' (ID: {parzelle_instance.id})") logger.debug(f"Parzelle instance before createParzelle: {parzelle_instance.model_dump(mode='json', exclude={'perimeter', 'baulinie', 'kontextInformationen'})}") parzelle_dict = parzelle_instance.model_dump(mode='json') logger.debug(f"Parzelle dict keys: {list(parzelle_dict.keys())}") created_parzelle = realEstateInterface.createParzelle(parzelle_instance) logger.info(f"createParzelle returned: ID={created_parzelle.id if created_parzelle else 'None'}, Label={created_parzelle.label if created_parzelle else 'None'}") if not created_parzelle: raise ValueError("Failed to create Parzelle - createParzelle returned None") if not created_parzelle.id: raise ValueError("Failed to create Parzelle - no ID returned") logger.info(f"Parzelle created with ID: {created_parzelle.id}") logger.debug(f"Verifying Parzelle {created_parzelle.id} exists in database...") verify_parzelle = realEstateInterface.getParzelle(created_parzelle.id) if not verify_parzelle: logger.error(f"Parzelle {created_parzelle.id} was not found in database after creation") all_parzellen = realEstateInterface.getParzellen(recordFilter=None) logger.error(f"Total Parzellen in database: {len(all_parzellen)}") if all_parzellen: logger.error(f"Sample Parzelle IDs: {[p.id for p in all_parzellen[:5]]}") raise ValueError(f"Parzelle {created_parzelle.id} was not found in database after creation") logger.info(f"Verified Parzelle {created_parzelle.id} exists in database") created_parzelle = verify_parzelle if created_parzelle.perimeter: parcel_perimeters.append(created_parzelle.perimeter) created_parzellen.append(created_parzelle) except Exception as e: logger.error(f"Error creating Parzelle {idx + 1}: {str(e)}", exc_info=True) raise if not created_parzellen: raise ValueError("No Parzellen were successfully created") logger.info(f"Successfully created {len(created_parzellen)} Parzelle(n)") project_baulinie = None if len(parcel_perimeters) > 0: try: if len(parcel_perimeters) == 1: project_baulinie = parcel_perimeters[0] logger.info("Using single parcel perimeter as baulinie") else: logger.info(f"Combining {len(parcel_perimeters)} parcel geometries to create baulinie") project_baulinie = combine_parcel_geometries(parcel_perimeters) logger.info(f"Created combined baulinie with {len(project_baulinie.punkte)} points") except Exception as e: logger.error(f"Error combining parcel geometries for baulinie: {e}", exc_info=True) if parcel_perimeters: project_baulinie = parcel_perimeters[0] logger.warning("Using first parcel perimeter as fallback baulinie") status_prozess_enum = None if status_prozess: try: if isinstance(status_prozess, str): status_prozess_enum = StatusProzess(status_prozess) elif isinstance(status_prozess, StatusProzess): status_prozess_enum = status_prozess except (ValueError, KeyError): logger.warning(f"Invalid statusProzess '{status_prozess}', using default 'Eingang'") status_prozess_enum = StatusProzess.EINGANG else: status_prozess_enum = StatusProzess.EINGANG logger.debug(f"Preparing Projekt creation with baulinie: {project_baulinie is not None}") if project_baulinie: logger.debug(f"Baulinie has {len(project_baulinie.punkte)} points") project_perimeter = created_parzellen[0].perimeter if created_parzellen else None projekt_create_data = { "mandateId": mandateId, "label": projekt_label, "statusProzess": status_prozess_enum, "perimeter": project_perimeter, "baulinie": project_baulinie, "parzellen": created_parzellen, "dokumente": [], "kontextInformationen": [], } logger.debug(f"Projekt data prepared: label={projekt_label}, parzellen_count={len(projekt_create_data['parzellen'])}, baulinie={'present' if project_baulinie else 'None'}") try: projekt_instance = Projekt(**projekt_create_data) logger.debug(f"Projekt instance created successfully with ID: {projekt_instance.id}") except Exception as e: logger.error(f"Error creating Projekt instance: {str(e)}", exc_info=True) raise logger.debug(f"Creating Projekt with {len(projekt_instance.parzellen)} Parzelle(n)") if projekt_instance.parzellen: for idx, p in enumerate(projekt_instance.parzellen): logger.debug(f" Parzelle {idx}: ID={p.id}, Label={p.label}") logger.debug(f"Projekt baulinie before save: {projekt_instance.baulinie is not None}") if projekt_instance.baulinie: logger.debug(f"Projekt baulinie has {len(projekt_instance.baulinie.punkte)} points") try: created_projekt = realEstateInterface.createProjekt(projekt_instance) logger.info(f"Created Projekt '{created_projekt.label}' (ID: {created_projekt.id})") logger.debug(f"Created Projekt baulinie: {created_projekt.baulinie is not None}") except Exception as e: logger.error(f"Error calling createProjekt: {str(e)}", exc_info=True) raise if not created_projekt or not created_projekt.id: raise ValueError("Failed to create Projekt - no ID returned") if not created_projekt.parzellen or len(created_projekt.parzellen) == 0: logger.warning(f"Projekt {created_projekt.id} created but no Parzellen linked") verify_projekt = realEstateInterface.getProjekt(created_projekt.id) if verify_projekt and verify_projekt.parzellen: logger.info(f"Parzellen found when fetching Projekt from database: {len(verify_projekt.parzellen)}") created_projekt = verify_projekt else: raise ValueError(f"Projekt {created_projekt.id} has no Parzellen linked after creation") else: logger.info(f"Projekt {created_projekt.id} successfully linked to {len(created_projekt.parzellen)} Parzelle(n)") for idx, p in enumerate(created_projekt.parzellen): logger.debug(f" Linked Parzelle {idx}: ID={p.id if hasattr(p, 'id') else 'NO ID'}, Label={p.label if hasattr(p, 'label') else 'NO LABEL'}") return { "projekt": created_projekt.model_dump(), "parzellen": [p.model_dump() for p in created_parzellen], } except HTTPException: raise except Exception as e: logger.error(f"Error creating project with parcel data: {str(e)}", exc_info=True) raise