from __future__ import annotations import uuid from sqlalchemy.orm import Session from geoalchemy2.shape import from_shape, to_shape from shapely.geometry import mapping from app.core.errors import AppError from app.models import Area, Project from app.schemas.area import AreaCreate, AreaRead, AreaUpdate from app.utils.geometry import area_m2, geometry_bbox_polygon, normalize_to_multipolygon class AreaService: @staticmethod def serialize_area(area: Area) -> dict: payload = AreaRead.model_validate(area).model_dump() payload["geometry"] = mapping(to_shape(area.geometry)) if area.geometry else None return payload @staticmethod def list_areas(db: Session, project_id: uuid.UUID, limit: int = 50, offset: int = 0) -> tuple[list[Area], int]: total = db.query(Area).filter(Area.project_id == project_id).count() areas = ( db.query(Area) .filter(Area.project_id == project_id) .order_by(Area.created_at.desc()) .offset(offset) .limit(limit) .all() ) return areas, total @staticmethod def create_area(db: Session, project_id: uuid.UUID, payload: AreaCreate) -> Area: if not db.get(Project, project_id): raise AppError(code="PROJECT_NOT_FOUND", message="Project not found", status_code=404) try: multipolygon = normalize_to_multipolygon(payload.geometry) except ValueError as exc: raise AppError(code="INVALID_GEOMETRY", message=str(exc), status_code=400) from exc area = Area( project_id=project_id, name=payload.name.strip() or "Unnamed area", geometry=from_shape(multipolygon, srid=4326), original_crs=payload.crs or "EPSG:4326", area_m2=area_m2(multipolygon), bbox=from_shape(geometry_bbox_polygon(multipolygon), srid=4326), ) db.add(area) db.commit() db.refresh(area) return area @staticmethod def get_area(db: Session, area_id: uuid.UUID) -> Area: area = db.get(Area, area_id) if not area: raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404) return area @staticmethod def update_area(db: Session, area_id: uuid.UUID, payload: AreaUpdate) -> Area: area = db.get(Area, area_id) if not area: raise AppError(code="AREA_NOT_FOUND", message="Area not found", status_code=404) changed = False if payload.name: area.name = payload.name.strip() or area.name changed = True if payload.crs: area.original_crs = payload.crs changed = True if not changed: raise AppError(code="NO_CHANGES", message="No updatable fields provided", status_code=422) db.add(area) db.commit() db.refresh(area) return area