fix:parzellen-fetching und geolinien

This commit is contained in:
Ida Dittrich 2025-12-15 09:22:42 +01:00
parent ce434dd74d
commit c56da4f51c
5 changed files with 1150 additions and 251 deletions

File diff suppressed because it is too large Load diff

View file

@ -12,13 +12,77 @@ from modules.datamodels.datamodelRealEstate import (
Projekt,
Parzelle,
StatusProzess,
GeoPolylinie,
)
from modules.services import getInterface as getServices
from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
from modules.connectors.connectorSwissTopoMapServer import SwissTopoMapServerConnector
logger = logging.getLogger(__name__)
# ===== Swisstopo Integration =====
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()
# Get GeoJSON feature from Swisstopo
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
# Convert GeoJSON to GeoPolylinie format
geometry = feature.get("geometry", {})
if geometry.get("type") == "Polygon":
coordinates = geometry.get("coordinates", [])
if coordinates and len(coordinates) > 0:
ring = coordinates[0] # Outer ring
punkte = []
for coord in ring:
if len(coord) >= 2:
punkt = {
"koordinatensystem": "LV95" if sr == 2056 else "WGS84",
"x": coord[0], # GeoJSON: [x, y] = [easting, northing]
"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
# ===== Direkte Query-Ausführung (stateless) =====
async def executeDirectQuery(
@ -600,6 +664,35 @@ async def executeIntentBasedOperation(
# Handle complex objects
if "perimeter" in parameters and parameters["perimeter"]:
parzelle_data["perimeter"] = GeoPolylinie(**parameters["perimeter"])
elif "kontextGemeinde" in parameters and parameters.get("kontextGemeinde"):
# Try to fetch polygon from Swisstopo if gemeinde and parzellen_nr are available
gemeinde = parameters.get("kontextGemeinde")
parzellen_nr = parameters.get("label") or parameters.get("parzellen_nr") or parameters.get("parzellennummer")
if gemeinde and parzellen_nr:
logger.info(f"Attempting to fetch polygon from Swisstopo for {gemeinde} {parzellen_nr}")
try:
# Try to resolve gemeinde name if it's an ID
gemeinde_name = gemeinde
if len(gemeinde) == 36: # UUID format
# Try to get gemeinde name from interface (realEstateInterface already initialized above)
gemeinde_obj = realEstateInterface.getGemeinde(gemeinde)
if gemeinde_obj:
gemeinde_name = gemeinde_obj.label
polygon_data = await fetch_parcel_polygon_from_swisstopo(
gemeinde=gemeinde_name,
parzellen_nr=str(parzellen_nr),
sr=2056
)
if polygon_data:
parzelle_data["perimeter"] = GeoPolylinie(**polygon_data)
logger.info(f"Successfully fetched and set perimeter from Swisstopo")
else:
logger.warning(f"Could not fetch polygon from Swisstopo for {gemeinde_name} {parzellen_nr}")
except Exception as e:
logger.warning(f"Error fetching polygon from Swisstopo (continuing without): {e}")
if "baulinie" in parameters and parameters["baulinie"]:
parzelle_data["baulinie"] = GeoPolylinie(**parameters["baulinie"])

View file

@ -5,7 +5,7 @@ Handles user access management and permission checks.
import logging
from typing import Dict, Any, List, Optional
from modules.datamodels.datamodelUam import User, UserPrivilege
from modules.datamodels.datamodelUam import User
logger = logging.getLogger(__name__)
@ -21,7 +21,7 @@ class RealEstateAccess:
self.currentUser = currentUser
self.mandateId = currentUser.mandateId
self.userId = currentUser.id
self.privilege = currentUser.privilege
self.roleLabels = currentUser.roleLabels or []
if not self.mandateId or not self.userId:
raise ValueError("Invalid user context: mandateId and userId are required")
@ -42,10 +42,10 @@ class RealEstateAccess:
filtered_records = []
# System admins see all records
if self.privilege == UserPrivilege.SYSADMIN:
if "sysadmin" in self.roleLabels:
filtered_records = recordset
# Admins see records in their mandate
elif self.privilege == UserPrivilege.ADMIN:
elif "admin" in self.roleLabels:
filtered_records = [r for r in recordset if r.get("mandateId", "-") == self.mandateId]
# Regular users see only their records
else:

View file

@ -17,7 +17,7 @@ import math
from modules.datamodels.datamodelUam import User, UserConnection, AuthAuthority, ConnectionStatus
from modules.datamodels.datamodelSecurity import Token
from modules.security.auth import getCurrentUser, limiter
from modules.auth import getCurrentUser, limiter
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
from modules.interfaces.interfaceDbAppObjects import getInterface
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp

View file

@ -10,7 +10,7 @@ from typing import Optional, Dict, Any, List, Union
from fastapi import APIRouter, HTTPException, Depends, Body, Request, Query, Path, status
# Import auth modules
from modules.security.auth import limiter, getCurrentUser
from modules.auth import limiter, getCurrentUser
# Import models
from modules.datamodels.datamodelUam import User
@ -653,70 +653,45 @@ async def search_parcel(
full_address = None
plz = None
# Try to get address by querying the address layer at the parcel centroid
if centroid:
try:
import aiohttp
# First, try to use geocoded address info if available (more accurate than centroid query)
geocoded_address = parcel_data.get('geocoded_address')
if geocoded_address:
full_address = geocoded_address.get('full_address')
plz = geocoded_address.get('plz')
municipality_name = geocoded_address.get('municipality')
logger.debug(f"Using geocoded address: {full_address}")
# If geocoded address not available, try to get address by querying the address layer
# Use query coordinates (where user clicked/geocoded) instead of parcel centroid
# This ensures we get the address at the exact location, not at the parcel center
query_coords = parcel_data.get('query_coordinates')
address_query_coords = query_coords if query_coords else centroid
if not full_address and address_query_coords:
query_x = address_query_coords['x']
query_y = address_query_coords['y']
logger.debug(f"Querying address layer at query coordinates: ({query_x}, {query_y})")
# Check if this was a coordinate search (not geocoded address)
is_coordinate_search = ',' in location and not any(c.isalpha() for c in location.split(',')[0])
# Use connector's helper method to query building layer
# Use tolerance=1 (minimum) for coordinate searches to get exact building
building_tolerance = 1 if is_coordinate_search else 10
building_result = await connector._query_building_layer(query_x, query_y, tolerance=building_tolerance, buffer=25)
if building_result:
addr_attrs = building_result.get("attributes", {})
logger.debug(f"Address layer attributes: {addr_attrs}")
# Use MapServer identify on address layer to get actual address
identify_url = "https://api3.geo.admin.ch/rest/services/api/MapServer/identify"
# Extract address using connector's helper method
address_info = connector._extract_address_from_building_attrs(addr_attrs)
full_address = address_info.get('full_address')
plz = address_info.get('plz')
municipality_name = address_info.get('municipality')
# Calculate extent around centroid
buffer = 100 # 100m buffer
map_extent = f"{centroid['x'] - buffer},{centroid['y'] - buffer},{centroid['x'] + buffer},{centroid['y'] + buffer}"
params = {
"geometry": f"{centroid['x']},{centroid['y']}",
"geometryType": "esriGeometryPoint",
"sr": "2056",
"layers": "all:ch.bfs.gebaeude_wohnungs_register", # Building/address layer
"tolerance": 50,
"mapExtent": map_extent,
"imageDisplay": "800,600,96",
"returnGeometry": "false",
"f": "json"
}
logger.debug(f"Querying address layer at centroid: ({centroid['x']}, {centroid['y']})")
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session:
async with session.get(identify_url, params=params) as response:
if response.status == 200:
address_data = await response.json()
address_results = address_data.get("results", [])
if address_results:
# Get first address result
addr_attrs = address_results[0].get("attributes", {})
logger.debug(f"Address layer attributes: {addr_attrs}")
# Extract address components (note: strname is a list!)
street_list = addr_attrs.get("strname", [])
street = street_list[0] if isinstance(street_list, list) and street_list else None
house_number = addr_attrs.get("deinr")
plz = addr_attrs.get("dplz4")
municipality_name = addr_attrs.get("dplzname") or addr_attrs.get("ggdename")
# Clean municipality name (remove canton suffix like "Wald ZH" -> "Wald")
if municipality_name and " " in municipality_name:
# Format is often "City ZH" or "City (ZH)"
municipality_name = municipality_name.split("(")[0].strip()
# Remove canton code at the end if present
parts = municipality_name.split()
if len(parts) > 1 and len(parts[-1]) == 2 and parts[-1].isupper():
municipality_name = " ".join(parts[:-1])
# Construct full address
if street and house_number and plz and municipality_name:
full_address = f"{street} {house_number}, {plz} {municipality_name}"
logger.debug(f"Constructed address: {full_address}")
else:
logger.debug("No address results from building layer")
else:
logger.debug(f"Address identify returned status {response.status}")
except Exception as e:
logger.debug(f"Could not query address layer: {e}")
if full_address:
logger.debug(f"Constructed address: {full_address}")
# If address not found via building layer, try to construct from available data
if not full_address:
@ -813,46 +788,90 @@ async def search_parcel(
}
# Fetch adjacent parcels if requested
if include_adjacent and centroid:
if include_adjacent and parcel_data and parcel_data.get("geometry"):
try:
# Search in a radius around the parcel centroid
# Note: This is a simplified approach - may need refinement
adjacent_parcels = []
# Use the connector's method to find neighboring parcels by sampling along the boundary
# This ensures we find all parcels that actually touch the selected parcel
selected_parcel_id = parcel_info["id"]
adjacent_parcels_raw = await connector.find_neighboring_parcels(
parcel_data=parcel_data,
selected_parcel_id=selected_parcel_id,
sample_distance=20.0, # Sample every 20 meters (balanced for coverage and speed)
max_sample_points=30, # Allow up to 30 points to ensure all vertices are covered
max_neighbors=15, # Find up to 15 neighbors
max_concurrent=50 # Process up to 50 queries concurrently (maximum parallelization)
)
# Search in 4 directions from centroid (N, S, E, W)
search_distance = 50 # meters
search_coords = [
(centroid["x"], centroid["y"] + search_distance), # North
(centroid["x"], centroid["y"] - search_distance), # South
(centroid["x"] + search_distance, centroid["y"]), # East
(centroid["x"] - search_distance, centroid["y"]), # West
]
# Convert adjacent parcels to include GeoJSON geometry (optimized, minimal logging)
def convert_parcel_geometry(adj_parcel: Dict[str, Any]) -> Dict[str, Any]:
"""Convert a single adjacent parcel to include GeoJSON geometry."""
adj_parcel_with_geo = {
"id": adj_parcel["id"],
"egrid": adj_parcel.get("egrid"),
"number": adj_parcel.get("number"),
"perimeter": adj_parcel.get("perimeter")
}
# Convert geometry to GeoJSON format if available
adj_geometry = adj_parcel.get("geometry")
adj_perimeter = adj_parcel.get("perimeter")
if adj_geometry:
# Handle ESRI format (rings)
if "rings" in adj_geometry and adj_geometry["rings"]:
ring = adj_geometry["rings"][0] # Outer ring
coordinates = [[[p[0], p[1]] for p in ring]]
adj_parcel_with_geo["geometry_geojson"] = {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": coordinates
},
"properties": {
"id": adj_parcel["id"],
"egrid": adj_parcel.get("egrid"),
"number": adj_parcel.get("number")
}
}
# Handle GeoJSON format
elif adj_geometry.get("type") == "Polygon":
adj_parcel_with_geo["geometry_geojson"] = {
"type": "Feature",
"geometry": adj_geometry,
"properties": {
"id": adj_parcel["id"],
"egrid": adj_parcel.get("egrid"),
"number": adj_parcel.get("number")
}
}
# If no geometry_geojson was created but we have perimeter, create it from perimeter
if "geometry_geojson" not in adj_parcel_with_geo and adj_perimeter and adj_perimeter.get("punkte"):
punkte = adj_perimeter["punkte"]
coordinates = [[[p["x"], p["y"]] for p in punkte]]
adj_parcel_with_geo["geometry_geojson"] = {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": coordinates
},
"properties": {
"id": adj_parcel["id"],
"egrid": adj_parcel.get("egrid"),
"number": adj_parcel.get("number")
}
}
return adj_parcel_with_geo
for coord in search_coords:
try:
adj_data = await connector.get_parcel_info(coord[0], coord[1], tolerance=5)
if adj_data:
adj_attrs = adj_data.get("attributes", {})
adj_id = adj_attrs.get("label") or adj_attrs.get("number")
# Don't include the same parcel
if adj_id != parcel_info["id"]:
# Check if already in list
if not any(p["id"] == adj_id for p in adjacent_parcels):
adjacent_parcels.append({
"id": adj_id,
"egrid": adj_attrs.get("egris_egrid"),
"number": adj_attrs.get("number")
})
except Exception as e:
logger.debug(f"No adjacent parcel found at {coord}: {e}")
continue
# Convert all parcels in parallel (using list comprehension for speed)
adjacent_parcels = [convert_parcel_geometry(adj_parcel) for adj_parcel in adjacent_parcels_raw]
response_data["adjacent_parcels"] = adjacent_parcels
logger.info(f"Found {len(adjacent_parcels)} adjacent parcels")
logger.info(f"Found {len(adjacent_parcels)} neighboring parcels for parcel {selected_parcel_id}")
except Exception as e:
logger.warning(f"Error fetching adjacent parcels: {e}")
logger.warning(f"Error fetching adjacent parcels: {e}", exc_info=True)
response_data["adjacent_parcels"] = []
return response_data