819 lines
35 KiB
Python
819 lines
35 KiB
Python
# Copyright (c) 2026 PowerOn AG
|
|
# All rights reserved.
|
|
"""
|
|
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
|