fix:parzellen-fetching und geolinien
This commit is contained in:
parent
ce434dd74d
commit
c56da4f51c
5 changed files with 1150 additions and 251 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue