platform-core/modules/features/realEstate/serviceGeometry.py
ValueOn AG cf0233f193
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 13s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped
refactor: architecture cleanup + fix scheduler Automation2Workflow error
Fix: add missing Automation2Workflow/Automation2WorkflowRun imports to interfaceFeatureGraphicalEditor.py (caused scheduler crash on boot)
Refactor: gdprDeletion via onUserDelete lifecycle hooks
Refactor: i18nBootSync accounting labels via app.py parameter injection
Refactor: serviceHub moved to serviceCenter/serviceHub.py
Split: teamsbot/service.py, realEstate/main, routeTrustee, routeBilling
Cleanup: remove obsolete methodTrustee, serviceExceptions shim
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 07:59:31 +02:00

817 lines
35 KiB
Python

"""
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']}"
))
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